test('dynamic component', () => {
expect(compile(`<component is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(`
- "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps } = require(\\"vue\\")
- const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+ "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require(\\"vue\\")
+ const { ssrRenderVNode: _ssrRenderVNode } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
- _push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), _mergeProps({ prop: \\"b\\" }, _attrs), null, _parent))
+ _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(\\"foo\\"), _mergeProps({ prop: \\"b\\" }, _attrs), null), _parent)
}"
`)
expect(compile(`<component :is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(`
- "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps } = require(\\"vue\\")
- const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+ "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require(\\"vue\\")
+ const { ssrRenderVNode: _ssrRenderVNode } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
- _push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: \\"b\\" }, _attrs), null, _parent))
+ _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: \\"b\\" }, _attrs), null), _parent)
}"
`)
})
import { registerRuntimeHelpers } from '@vue/compiler-dom'
export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
+export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`)
export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
export const ssrHelpers = {
[SSR_INTERPOLATE]: `ssrInterpolate`,
+ [SSR_RENDER_VNODE]: `ssrRenderVNode`,
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
[SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_CLASS]: `ssrRenderClass`,
buildSlots,
FunctionExpression,
TemplateChildNode,
- TELEPORT,
createIfStatement,
createSimpleExpression,
getBaseTransformPreset,
ExpressionNode,
TemplateNode,
SUSPENSE,
- TRANSITION_GROUP
+ TELEPORT,
+ TRANSITION_GROUP,
+ CREATE_VNODE,
+ CallExpression
} from '@vue/compiler-dom'
-import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
+import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
import {
SSRTransformContext,
processChildren,
vnodeBranch: ReturnStatement
}
-const componentTypeMap = new WeakMap<ComponentNode, symbol>()
+const componentTypeMap = new WeakMap<
+ ComponentNode,
+ string | symbol | CallExpression
+>()
// ssr component transform is done in two phases:
// In phase 1. we use `buildSlot` to analyze the children of the component into
}
const component = resolveComponentType(node, context, true /* ssr */)
+ componentTypeMap.set(node, component)
+
if (isSymbol(component)) {
- componentTypeMap.set(node, component)
if (component === SUSPENSE) {
return ssrTransformSuspense(node, context)
}
? buildSlots(node, context, buildSSRSlotFn).slots
: `null`
- node.ssrCodegenNode = createCallExpression(
- context.helper(SSR_RENDER_COMPONENT),
- [component, props, slots, `_parent`]
- )
+ if (typeof component !== 'string') {
+ // dynamic component that resolved to a `resolveDynamicComponent` call
+ // expression - since the reoslved result may be a plain element (string)
+ // or a VNode, handle it with `renderVNode`.
+ node.ssrCodegenNode = createCallExpression(
+ context.helper(SSR_RENDER_VNODE),
+ [
+ `_push`,
+ createCallExpression(context.helper(CREATE_VNODE), [
+ component,
+ props,
+ slots
+ ]),
+ `_parent`
+ ]
+ )
+ } else {
+ node.ssrCodegenNode = createCallExpression(
+ context.helper(SSR_RENDER_COMPONENT),
+ [component, props, slots, `_parent`]
+ )
+ }
}
}
node: ComponentNode,
context: SSRTransformContext
) {
+ const component = componentTypeMap.get(node)!
if (!node.ssrCodegenNode) {
// this is a built-in component that fell-through.
- const component = componentTypeMap.get(node)!
if (component === TELEPORT) {
return ssrProcessTeleport(node, context)
} else if (component === SUSPENSE) {
vnodeBranch
)
}
- context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))
+ if (typeof component === 'string') {
+ // static component
+ context.pushStatement(
+ createCallExpression(`_push`, [node.ssrCodegenNode])
+ )
+ } else {
+ // dynamic component (`resolveDynamicComponent` call)
+ // the codegen node is a `renderVNode` call
+ context.pushStatement(node.ssrCodegenNode)
+ }
}
}
--- /dev/null
+import { createApp, createVNode } from 'vue'
+import { renderToString } from '../src/renderToString'
+
+describe('ssr: dynamic component', () => {
+ test('resolved to component', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: {
+ template: `<div><slot/></div>`
+ }
+ },
+ template: `<component :is="'one'"><span>slot</span></component>`
+ })
+ )
+ ).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
+ })
+
+ test('resolve to element', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ template: `<component :is="'p'"><span>slot</span></component>`
+ })
+ )
+ ).toBe(`<p><span>slot</span></p>`)
+ })
+
+ test('resolve to component vnode', async () => {
+ const Child = {
+ props: ['id'],
+ template: `<div>{{ id }}<slot/></div>`
+ }
+ expect(
+ await renderToString(
+ createApp({
+ setup() {
+ return {
+ vnode: createVNode(Child, { id: 'test' })
+ }
+ },
+ template: `<component :is="vnode"><span>slot</span></component>`
+ })
+ )
+ ).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
+ })
+
+ test('resolve to element vnode', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ setup() {
+ return {
+ vnode: createVNode('div', { id: 'test' })
+ }
+ },
+ template: `<component :is="vnode"><span>slot</span></component>`
+ })
+ )
+ ).toBe(`<div id="test"><span>slot</span></div>`)
+ })
+})
export { renderToStream } from './renderToStream'
// internal runtime helpers
+export { renderVNode as ssrRenderVNode } from './render'
export { ssrRenderComponent } from './helpers/ssrRenderComponent'
export { ssrRenderSlot } from './helpers/ssrRenderSlot'
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
return getBuffer()
}
-function renderVNode(
+export function renderVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance