| 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 {
// 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'
-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 <slot name="xxx" OR :name="xxx" />
- 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)
)
}
}
+
+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 <slot name="xxx" OR :name="xxx" />
+ 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
+ }
+}
'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), [
)
// target-agnostic transform used for both Client and SSR
-export function processForNode(
+export function processFor(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
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 () => {
)
// target-agnostic transform used for both Client and SSR
-export function processIfBranches(
+export function processIf(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
--- /dev/null
+import { compile } from '../src'
+
+describe('ssr: <slot>', () => {
+ test('basic', () => {
+ expect(compile(`<slot/>`).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(`<slot name="foo" />`).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(`<slot :name="bar.baz" />`).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(`<slot name="foo" :p="1" bar="2" />`).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(`<slot>some {{ fallback }} content</slot>`).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)
+ }"
+ `)
+ })
+})
} 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
} 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))
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)
}
}
}
-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!)
+}
import {
createStructuralDirectiveTransform,
ForNode,
- processForNode,
+ processFor,
createCallExpression,
createFunctionExpression,
createForLoopParams,
// 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
import {
createStructuralDirectiveTransform,
- processIfBranches,
+ processIf,
IfNode,
createIfStatement,
createBlockStatement,
// 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!,
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 () => {
props: ['msg'],
ssrRender(ctx: any, push: any, parent: any) {
push(`<div class="child">`)
- renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent)
+ renderSlot(
+ ctx.$slots,
+ 'default',
+ { msg: 'from slot' },
+ () => {
+ push(`fallback`)
+ },
+ push,
+ parent
+ )
push(`</div>`)
}
}
`<!----><span>from slot</span><!---->` +
`</div></div>`
)
+
+ // test fallback
+ expect(
+ await renderToString(
+ createApp({
+ ssrRender(_ctx, push, parent) {
+ push(`<div>parent`)
+ push(renderComponent(Child, { msg: 'hello' }, null, parent))
+ push(`</div>`)
+ }
+ })
+ )
+ ).toBe(`<div>parent<div class="child"><!---->fallback<!----></div></div>`)
})
test('nested components with vnode slots', async () => {
props: ['msg'],
ssrRender(ctx: any, push: any, parent: any) {
push(`<div class="child">`)
- renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent)
+ renderSlot(
+ ctx.$slots,
+ 'default',
+ { msg: 'from slot' },
+ null,
+ push,
+ parent
+ )
push(`</div>`)
}
}
--- /dev/null
+import { Props, PushFn, renderVNodeChildren } from '../renderToString'
+import { ComponentInternalInstance, Slot, Slots } from 'vue'
+
+export type SSRSlots = Record<string, SSRSlot>
+
+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(`<!---->`)
+}
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,
Portal,
ShapeFlags,
ssrUtils,
- Slot,
Slots
} from 'vue'
import {
escapeHtml
} from '@vue/shared'
import { renderAttrs } from './helpers/renderAttrs'
+import { SSRSlots } from './helpers/renderSlot'
const {
isVNode,
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
-type PushFn = (item: SSRBufferItem) => void
-type Props = Record<string, unknown>
+export type PushFn = (item: SSRBufferItem) => void
+export type Props = Record<string, unknown>
function createBuffer() {
let appendable = false
}
}
-function renderVNodeChildren(
+export function renderVNodeChildren(
push: PushFn,
children: VNodeArrayChildren,
parentComponent: ComponentInternalInstance | null = null
push(`</${tag}>`)
}
}
-
-export type SSRSlots = Record<string, SSRSlot>
-
-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(`<!---->`)
-}