Skip to content

Commit 80c625d

Browse files
committed
feat(ssr): compiler-ssr support for Suspense
1 parent 47ead3b commit 80c625d

File tree

10 files changed

+396
-158
lines changed

10 files changed

+396
-158
lines changed

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

+1-26
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('ssr: components', () => {
3434
.toMatchInlineSnapshot(`
3535
"const { resolveDynamicComponent: _resolveDynamicComponent } = require(\\"vue\\")
3636
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
37-
37+
3838
return function ssrRender(_ctx, _push, _parent) {
3939
_push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo, _ctx.$), { prop: \\"b\\" }, null, _parent))
4040
}"
@@ -269,7 +269,6 @@ describe('ssr: components', () => {
269269
})
270270

271271
test('built-in fallthroughs', () => {
272-
// no fragment
273272
expect(compile(`<transition><div/></transition>`).code)
274273
.toMatchInlineSnapshot(`
275274
"
@@ -278,7 +277,6 @@ describe('ssr: components', () => {
278277
}"
279278
`)
280279

281-
// wrap with fragment
282280
expect(compile(`<transition-group><div/></transition-group>`).code)
283281
.toMatchInlineSnapshot(`
284282
"
@@ -287,7 +285,6 @@ describe('ssr: components', () => {
287285
}"
288286
`)
289287

290-
// no fragment
291288
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
292289
.toMatchInlineSnapshot(`
293290
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
@@ -299,28 +296,6 @@ describe('ssr: components', () => {
299296
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
300297
}"
301298
`)
302-
303-
// wrap with fragment
304-
expect(compile(`<suspense><div/></suspense>`).code)
305-
.toMatchInlineSnapshot(`
306-
"
307-
return function ssrRender(_ctx, _push, _parent) {
308-
_push(\`<div></div>\`)
309-
}"
310-
`)
311-
})
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-
`)
324299
})
325300
})
326301
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { compile } from '../src'
2+
3+
describe('ssr compile: portal', () => {
4+
test('should work', () => {
5+
expect(compile(`<portal :target="target"><div/></portal>`).code)
6+
.toMatchInlineSnapshot(`
7+
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
8+
9+
return function ssrRender(_ctx, _push, _parent) {
10+
_ssrRenderPortal((_push) => {
11+
_push(\`<div></div>\`)
12+
}, _ctx.target, _parent)
13+
}"
14+
`)
15+
})
16+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { compile } from '../src'
2+
3+
describe('ssr compile: suspense', () => {
4+
test('implicit default', () => {
5+
expect(compile(`<suspense><foo/></suspense>`).code).toMatchInlineSnapshot(`
6+
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
7+
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require(\\"@vue/server-renderer\\")
8+
9+
return function ssrRender(_ctx, _push, _parent) {
10+
const _component_foo = _resolveComponent(\\"foo\\")
11+
12+
_push(_ssrRenderSuspense({
13+
default: (_push) => {
14+
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
15+
},
16+
_: 1
17+
}))
18+
}"
19+
`)
20+
})
21+
22+
test('explicit slots', () => {
23+
expect(
24+
compile(`<suspense>
25+
<template #default>
26+
<foo/>
27+
</template>
28+
<template #fallback>
29+
loading...
30+
</template>
31+
</suspense>`).code
32+
).toMatchInlineSnapshot(`
33+
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
34+
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require(\\"@vue/server-renderer\\")
35+
36+
return function ssrRender(_ctx, _push, _parent) {
37+
const _component_foo = _resolveComponent(\\"foo\\")
38+
39+
_push(_ssrRenderSuspense({
40+
default: (_push) => {
41+
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
42+
},
43+
fallback: (_push) => {
44+
_push(\` loading... \`)
45+
},
46+
_: 1
47+
}))
48+
}"
49+
`)
50+
})
51+
})

packages/compiler-ssr/src/runtimeHelpers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ 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`)
1616
export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`)
17+
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
1718

1819
export const ssrHelpers = {
1920
[SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -29,7 +30,8 @@ export const ssrHelpers = {
2930
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
3031
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
3132
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
32-
[SSR_RENDER_PORTAL]: `ssrRenderPortal`
33+
[SSR_RENDER_PORTAL]: `ssrRenderPortal`,
34+
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
3335
}
3436

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

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

+21-47
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@ import {
3030
traverseNode,
3131
ExpressionNode,
3232
TemplateNode,
33-
findProp,
34-
JSChildNode
33+
SUSPENSE
3534
} from '@vue/compiler-dom'
36-
import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers'
35+
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
3736
import {
3837
SSRTransformContext,
3938
processChildren,
4039
processChildrenAsStatement
4140
} from '../ssrCodegenTransform'
41+
import { ssrProcessPortal } from './ssrTransformPortal'
42+
import {
43+
ssrProcessSuspense,
44+
ssrTransformSuspense
45+
} from './ssrTransformSuspense'
4246
import { isSymbol, isObject, isArray } from '@vue/shared'
43-
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
4447

4548
// We need to construct the slot functions in the 1st pass to ensure proper
4649
// scope tracking, but the children of each slot cannot be processed until
@@ -56,6 +59,12 @@ interface WIPSlotEntry {
5659

5760
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
5861

62+
// ssr component transform is done in two phases:
63+
// In phase 1. we use `buildSlot` to analyze the children of the component into
64+
// WIP slot functions (it must be done in phase 1 because `buildSlot` relies on
65+
// the core transform context).
66+
// In phase 2. we convert the WIP slots from phase 1 into ssr-specific codegen
67+
// nodes.
5968
export const ssrTransformComponent: NodeTransform = (node, context) => {
6069
if (
6170
node.type !== NodeTypes.ELEMENT ||
@@ -67,6 +76,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
6776
const component = resolveComponentType(node, context, true /* ssr */)
6877
if (isSymbol(component)) {
6978
componentTypeMap.set(node, component)
79+
if (component === SUSPENSE) {
80+
return ssrTransformSuspense(node, context)
81+
}
7082
return // built-in component: fallthrough
7183
}
7284

@@ -132,12 +144,15 @@ export function ssrProcessComponent(
132144
) {
133145
if (!node.ssrCodegenNode) {
134146
// this is a built-in component that fell-through.
135-
// just render its children.
136147
const component = componentTypeMap.get(node)!
137148
if (component === PORTAL) {
138149
return ssrProcessPortal(node, context)
150+
} else if (component === SUSPENSE) {
151+
return ssrProcessSuspense(node, context)
152+
} else {
153+
// real fall-through (e.g. KeepAlive): just render its children.
154+
processChildren(node.children, context)
139155
}
140-
processChildren(node.children, context)
141156
} else {
142157
// finish up slot function expressions from the 1st pass.
143158
const wipEntries = wipMap.get(node) || []
@@ -161,47 +176,6 @@ export function ssrProcessComponent(
161176
}
162177
}
163178

164-
function ssrProcessPortal(node: ComponentNode, context: SSRTransformContext) {
165-
const targetProp = findProp(node, 'target')
166-
if (!targetProp) {
167-
context.onError(
168-
createSSRCompilerError(SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, node.loc)
169-
)
170-
return
171-
}
172-
173-
let target: JSChildNode
174-
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
175-
target = createSimpleExpression(targetProp.value.content, true)
176-
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
177-
target = targetProp.exp
178-
} else {
179-
context.onError(
180-
createSSRCompilerError(
181-
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
182-
targetProp.loc
183-
)
184-
)
185-
return
186-
}
187-
188-
const contentRenderFn = createFunctionExpression(
189-
[`_push`],
190-
undefined, // Body is added later
191-
true, // newline
192-
false, // isSlot
193-
node.loc
194-
)
195-
contentRenderFn.body = processChildrenAsStatement(node.children, context)
196-
context.pushStatement(
197-
createCallExpression(context.helper(SSR_RENDER_PORTAL), [
198-
contentRenderFn,
199-
target,
200-
`_parent`
201-
])
202-
)
203-
}
204-
205179
export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
206180

207181
const [baseNodeTransforms, baseDirectiveTransforms] = getBaseTransformPreset(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
ComponentNode,
3+
findProp,
4+
JSChildNode,
5+
NodeTypes,
6+
createSimpleExpression,
7+
createFunctionExpression,
8+
createCallExpression
9+
} from '@vue/compiler-dom'
10+
import {
11+
SSRTransformContext,
12+
processChildrenAsStatement
13+
} from '../ssrCodegenTransform'
14+
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
15+
import { SSR_RENDER_PORTAL } from '../runtimeHelpers'
16+
17+
// Note: this is a 2nd-pass codegen transform.
18+
export function ssrProcessPortal(
19+
node: ComponentNode,
20+
context: SSRTransformContext
21+
) {
22+
const targetProp = findProp(node, 'target')
23+
if (!targetProp) {
24+
context.onError(
25+
createSSRCompilerError(SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, node.loc)
26+
)
27+
return
28+
}
29+
30+
let target: JSChildNode
31+
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
32+
target = createSimpleExpression(targetProp.value.content, true)
33+
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
34+
target = targetProp.exp
35+
} else {
36+
context.onError(
37+
createSSRCompilerError(
38+
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
39+
targetProp.loc
40+
)
41+
)
42+
return
43+
}
44+
45+
const contentRenderFn = createFunctionExpression(
46+
[`_push`],
47+
undefined, // Body is added later
48+
true, // newline
49+
false, // isSlot
50+
node.loc
51+
)
52+
contentRenderFn.body = processChildrenAsStatement(node.children, context)
53+
context.pushStatement(
54+
createCallExpression(context.helper(SSR_RENDER_PORTAL), [
55+
contentRenderFn,
56+
target,
57+
`_parent`
58+
])
59+
)
60+
}

0 commit comments

Comments
 (0)