diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 0cca3cefee2..8efe67a5c9a 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -309,5 +309,18 @@ describe('ssr: components', () => { }" `) }) + + test('portal rendering', () => { + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _ssrRenderPortal((_push) => { + _push(\`
\`) + }, _ctx.target, _parent) + }" + `) + }) }) }) diff --git a/packages/compiler-ssr/src/errors.ts b/packages/compiler-ssr/src/errors.ts index 30431fb0818..fdc99160b01 100644 --- a/packages/compiler-ssr/src/errors.ts +++ b/packages/compiler-ssr/src/errors.ts @@ -18,10 +18,12 @@ export function createSSRCompilerError( export const enum SSRErrorCodes { X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__, - X_SSR_UNSAFE_ATTR_NAME + X_SSR_UNSAFE_ATTR_NAME, + X_SSR_NO_PORTAL_TARGET } export const SSRErrorMessages: { [code: number]: string } = { [SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.`, - [SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.` + [SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`, + [SSRErrorCodes.X_SSR_NO_PORTAL_TARGET]: `No target prop on portal element.` } diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 2f394514de6..01875cf05ca 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -13,6 +13,7 @@ export const SSR_LOOSE_EQUAL = Symbol(`ssrLooseEqual`) export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`) export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`) export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`) +export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`) export const ssrHelpers = { [SSR_INTERPOLATE]: `ssrInterpolate`, @@ -27,7 +28,8 @@ export const ssrHelpers = { [SSR_LOOSE_EQUAL]: `ssrLooseEqual`, [SSR_LOOSE_CONTAIN]: `ssrLooseContain`, [SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`, - [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps` + [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`, + [SSR_RENDER_PORTAL]: `ssrRenderPortal` } // Note: these are helpers imported from @vue/server-renderer diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index a9c38c70cfa..87c6988bfb8 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -31,9 +31,11 @@ import { createTransformContext, traverseNode, ExpressionNode, - TemplateNode + TemplateNode, + findProp, + JSChildNode } from '@vue/compiler-dom' -import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' +import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers' import { SSRTransformContext, processChildren, @@ -134,7 +136,34 @@ export function ssrProcessComponent( const component = componentTypeMap.get(node)! if (component === PORTAL) { - // TODO + const targetProp = findProp(node, 'target') + if (!targetProp) return + + let target: JSChildNode + if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) { + target = createSimpleExpression(targetProp.value.content, true) + } else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) { + target = targetProp.exp + } else { + return + } + + const contentRenderFn = createFunctionExpression( + [`_push`], + undefined, // Body is added later + true, // newline + false, // isSlot + node.loc + ) + contentRenderFn.body = processChildrenAsStatement(node.children, context) + context.pushStatement( + createCallExpression(context.helper(SSR_RENDER_PORTAL), [ + contentRenderFn, + target, + `_parent` + ]) + ) + return } diff --git a/packages/server-renderer/__tests__/ssrRenderPortal.spec.ts b/packages/server-renderer/__tests__/ssrRenderPortal.spec.ts new file mode 100644 index 00000000000..70031dd4ee0 --- /dev/null +++ b/packages/server-renderer/__tests__/ssrRenderPortal.spec.ts @@ -0,0 +1,29 @@ +import { createApp } from 'vue' +import { renderToString, SSRContext } from '../src/renderToString' +import { ssrRenderPortal } from '../src/helpers/ssrRenderPortal' + +describe('ssrRenderPortal', () => { + test('portal rendering', async () => { + const ctx = { + portals: {} + } as SSRContext + await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + ssrRender(_ctx, _push, _parent) { + ssrRenderPortal( + _push => { + _push(`
content
`) + }, + '#target', + _parent + ) + } + }), + ctx + ) + expect(ctx.portals!['#target']).toBe(`
content
`) + }) +}) diff --git a/packages/server-renderer/src/helpers/ssrRenderPortal.ts b/packages/server-renderer/src/helpers/ssrRenderPortal.ts new file mode 100644 index 00000000000..12c2282334a --- /dev/null +++ b/packages/server-renderer/src/helpers/ssrRenderPortal.ts @@ -0,0 +1,20 @@ +import { ComponentInternalInstance, ssrContextKey } from 'vue' +import { SSRContext, createBuffer, PushFn } from '../renderToString' + +export function ssrRenderPortal( + contentRenderFn: (push: PushFn) => void, + target: string, + parentComponent: ComponentInternalInstance +) { + const { getBuffer, push } = createBuffer() + + contentRenderFn(push) + + const context = parentComponent.appContext.provides[ + ssrContextKey as any + ] as SSRContext + const portalBuffers = + context.__portalBuffers || (context.__portalBuffers = {}) + + portalBuffers[target] = getBuffer() +} diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 59b2b45f8fb..315b3ae18c7 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -13,6 +13,7 @@ export { } from './helpers/ssrRenderAttrs' export { ssrInterpolate } from './helpers/ssrInterpolate' export { ssrRenderList } from './helpers/ssrRenderList' +export { ssrRenderPortal } from './helpers/ssrRenderPortal' // v-model helpers export { diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 0aac04cd14a..14c041c6561 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -45,9 +45,12 @@ const { // - A resolved buffer (recursive arrays of strings that can be unrolled // synchronously) // - An async buffer (a Promise that resolves to a resolved buffer) -type SSRBuffer = SSRBufferItem[] -type SSRBufferItem = string | ResolvedSSRBuffer | Promise -type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[] +export type SSRBuffer = SSRBufferItem[] +export type SSRBufferItem = + | string + | ResolvedSSRBuffer + | Promise +export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[] export type PushFn = (item: SSRBufferItem) => void @@ -62,7 +65,7 @@ export type SSRContext = { > } -function createBuffer() { +export function createBuffer() { let appendable = false let hasAsync = false const buffer: SSRBuffer = []