Skip to content

Commit d8ed0e7

Browse files
authored
feat(compiler-ssr): compile portal (#775)
1 parent 312513d commit d8ed0e7

File tree

8 files changed

+109
-10
lines changed

8 files changed

+109
-10
lines changed

packages/compiler-ssr/__tests__/ssrComponent.spec.ts

+13
Original file line numberDiff line numberDiff line change
@@ -309,5 +309,18 @@ describe('ssr: components', () => {
309309
}"
310310
`)
311311
})
312+
313+
test('portal rendering', () => {
314+
expect(compile(`<portal :target="target"><div/></portal>`).code)
315+
.toMatchInlineSnapshot(`
316+
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
317+
318+
return function ssrRender(_ctx, _push, _parent) {
319+
_ssrRenderPortal((_push) => {
320+
_push(\`<div></div>\`)
321+
}, _ctx.target, _parent)
322+
}"
323+
`)
324+
})
312325
})
313326
})

packages/compiler-ssr/src/errors.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ export function createSSRCompilerError(
1818

1919
export const enum SSRErrorCodes {
2020
X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__,
21-
X_SSR_UNSAFE_ATTR_NAME
21+
X_SSR_UNSAFE_ATTR_NAME,
22+
X_SSR_NO_PORTAL_TARGET
2223
}
2324

2425
export const SSRErrorMessages: { [code: number]: string } = {
2526
[SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.`,
26-
[SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`
27+
[SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`,
28+
[SSRErrorCodes.X_SSR_NO_PORTAL_TARGET]: `No target prop on portal element.`
2729
}

packages/compiler-ssr/src/runtimeHelpers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const SSR_LOOSE_EQUAL = Symbol(`ssrLooseEqual`)
1313
export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
1414
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
1515
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
16+
export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`)
1617

1718
export const ssrHelpers = {
1819
[SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -27,7 +28,8 @@ export const ssrHelpers = {
2728
[SSR_LOOSE_EQUAL]: `ssrLooseEqual`,
2829
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
2930
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
30-
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`
31+
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
32+
[SSR_RENDER_PORTAL]: `ssrRenderPortal`
3133
}
3234

3335
// Note: these are helpers imported from @vue/server-renderer

packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

+32-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ import {
3131
createTransformContext,
3232
traverseNode,
3333
ExpressionNode,
34-
TemplateNode
34+
TemplateNode,
35+
findProp,
36+
JSChildNode
3537
} from '@vue/compiler-dom'
36-
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
38+
import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers'
3739
import {
3840
SSRTransformContext,
3941
processChildren,
@@ -134,7 +136,34 @@ export function ssrProcessComponent(
134136
const component = componentTypeMap.get(node)!
135137

136138
if (component === PORTAL) {
137-
// TODO
139+
const targetProp = findProp(node, 'target')
140+
if (!targetProp) return
141+
142+
let target: JSChildNode
143+
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
144+
target = createSimpleExpression(targetProp.value.content, true)
145+
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
146+
target = targetProp.exp
147+
} else {
148+
return
149+
}
150+
151+
const contentRenderFn = createFunctionExpression(
152+
[`_push`],
153+
undefined, // Body is added later
154+
true, // newline
155+
false, // isSlot
156+
node.loc
157+
)
158+
contentRenderFn.body = processChildrenAsStatement(node.children, context)
159+
context.pushStatement(
160+
createCallExpression(context.helper(SSR_RENDER_PORTAL), [
161+
contentRenderFn,
162+
target,
163+
`_parent`
164+
])
165+
)
166+
138167
return
139168
}
140169

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createApp } from 'vue'
2+
import { renderToString, SSRContext } from '../src/renderToString'
3+
import { ssrRenderPortal } from '../src/helpers/ssrRenderPortal'
4+
5+
describe('ssrRenderPortal', () => {
6+
test('portal rendering', async () => {
7+
const ctx = {
8+
portals: {}
9+
} as SSRContext
10+
await renderToString(
11+
createApp({
12+
data() {
13+
return { msg: 'hello' }
14+
},
15+
ssrRender(_ctx, _push, _parent) {
16+
ssrRenderPortal(
17+
_push => {
18+
_push(`<div>content</div>`)
19+
},
20+
'#target',
21+
_parent
22+
)
23+
}
24+
}),
25+
ctx
26+
)
27+
expect(ctx.portals!['#target']).toBe(`<div>content</div>`)
28+
})
29+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ComponentInternalInstance, ssrContextKey } from 'vue'
2+
import { SSRContext, createBuffer, PushFn } from '../renderToString'
3+
4+
export function ssrRenderPortal(
5+
contentRenderFn: (push: PushFn) => void,
6+
target: string,
7+
parentComponent: ComponentInternalInstance
8+
) {
9+
const { getBuffer, push } = createBuffer()
10+
11+
contentRenderFn(push)
12+
13+
const context = parentComponent.appContext.provides[
14+
ssrContextKey as any
15+
] as SSRContext
16+
const portalBuffers =
17+
context.__portalBuffers || (context.__portalBuffers = {})
18+
19+
portalBuffers[target] = getBuffer()
20+
}

packages/server-renderer/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {
1313
} from './helpers/ssrRenderAttrs'
1414
export { ssrInterpolate } from './helpers/ssrInterpolate'
1515
export { ssrRenderList } from './helpers/ssrRenderList'
16+
export { ssrRenderPortal } from './helpers/ssrRenderPortal'
1617

1718
// v-model helpers
1819
export {

packages/server-renderer/src/renderToString.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ const {
4545
// - A resolved buffer (recursive arrays of strings that can be unrolled
4646
// synchronously)
4747
// - An async buffer (a Promise that resolves to a resolved buffer)
48-
type SSRBuffer = SSRBufferItem[]
49-
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
50-
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
48+
export type SSRBuffer = SSRBufferItem[]
49+
export type SSRBufferItem =
50+
| string
51+
| ResolvedSSRBuffer
52+
| Promise<ResolvedSSRBuffer>
53+
export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
5154

5255
export type PushFn = (item: SSRBufferItem) => void
5356

@@ -62,7 +65,7 @@ export type SSRContext = {
6265
>
6366
}
6467

65-
function createBuffer() {
68+
export function createBuffer() {
6669
let appendable = false
6770
let hasAsync = false
6871
const buffer: SSRBuffer = []

0 commit comments

Comments
 (0)