Portal,
ssrUtils,
Slots,
- warn
+ warn,
+ createApp
} from 'vue'
import {
ShapeFlags,
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
+
export type PushFn = (item: SSRBufferItem) => void
+
export type Props = Record<string, unknown>
+const ssrContextKey = Symbol()
+
+export type SSRContext = {
+ [key: string]: any
+ portals?: Record<string, string>
+ __portalBuffers?: Record<
+ string,
+ ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
+ >
+}
+
function createBuffer() {
let appendable = false
let hasAsync = false
return ret
}
-export async function renderToString(input: App | VNode): Promise<string> {
+export async function renderToString(
+ input: App | VNode,
+ context: SSRContext = {}
+): Promise<string> {
let buffer: ResolvedSSRBuffer
if (isVNode(input)) {
- // raw vnode, wrap with component
- buffer = await renderComponent({ render: () => input })
+ // raw vnode, wrap with app (for context)
+ return renderToString(createApp({ render: () => input }), context)
} else {
// rendering an app
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
+ // provide the ssr context to the tree
+ input.provide(ssrContextKey, context)
buffer = await renderComponentVNode(vnode)
}
+
+ // resolve portals
+ if (context.__portalBuffers) {
+ context.portals = context.portals || {}
+ for (const key in context.__portalBuffers) {
+ // note: it's OK to await sequentially here because the Promises were
+ // created eagerly in parallel.
+ context.portals[key] = unrollBuffer(await context.__portalBuffers[key])
+ }
+ }
+
return unrollBuffer(buffer)
}
}
type SSRRenderFunction = (
- ctx: any,
+ context: any,
push: (item: any) => void,
parentInstance: ComponentInternalInstance
) => void
function renderVNode(
push: PushFn,
vnode: VNode,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
const { type, shapeFlag, children } = vnode
switch (type) {
push(`<!---->`)
break
case Portal:
- // TODO
+ renderPortal(vnode, parentComponent)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
export function renderVNodeChildren(
push: PushFn,
children: VNodeArrayChildren,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
for (let i = 0; i < children.length; i++) {
renderVNode(push, normalizeVNode(children[i]), parentComponent)
function renderElement(
push: PushFn,
vnode: VNode,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
const tag = vnode.type as string
const { props, children, shapeFlag, scopeId } = vnode
push(`</${tag}>`)
}
}
+
+function renderPortal(
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance
+) {
+ const target = vnode.props && vnode.props.target
+ if (!target) {
+ console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
+ return []
+ }
+ if (!isString(target)) {
+ console.warn(
+ `[@vue/server-renderer] Portal target must be a query selector string.`
+ )
+ return []
+ }
+
+ const { buffer, push, hasAsync } = createBuffer()
+ renderVNodeChildren(
+ push,
+ vnode.children as VNodeArrayChildren,
+ parentComponent
+ )
+ const context = parentComponent.appContext.provides[
+ ssrContextKey as any
+ ] as SSRContext
+ const portalBuffers =
+ context.__portalBuffers || (context.__portalBuffers = {})
+ portalBuffers[target] = hasAsync()
+ ? Promise.all(buffer)
+ : (buffer as ResolvedSSRBuffer)
+}