feat(ssr): compiler-ssr support for Suspense
This commit is contained in:
parent
47ead3b33a
commit
80c625dce3
@ -34,7 +34,7 @@ describe('ssr: components', () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"const { resolveDynamicComponent: _resolveDynamicComponent } = require(\\"vue\\")
|
"const { resolveDynamicComponent: _resolveDynamicComponent } = require(\\"vue\\")
|
||||||
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
|
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
_push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo, _ctx.$), { prop: \\"b\\" }, null, _parent))
|
_push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo, _ctx.$), { prop: \\"b\\" }, null, _parent))
|
||||||
}"
|
}"
|
||||||
@ -269,7 +269,6 @@ describe('ssr: components', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('built-in fallthroughs', () => {
|
test('built-in fallthroughs', () => {
|
||||||
// no fragment
|
|
||||||
expect(compile(`<transition><div/></transition>`).code)
|
expect(compile(`<transition><div/></transition>`).code)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"
|
"
|
||||||
@ -278,7 +277,6 @@ describe('ssr: components', () => {
|
|||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// wrap with fragment
|
|
||||||
expect(compile(`<transition-group><div/></transition-group>`).code)
|
expect(compile(`<transition-group><div/></transition-group>`).code)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"
|
"
|
||||||
@ -287,7 +285,6 @@ describe('ssr: components', () => {
|
|||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// no fragment
|
|
||||||
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
|
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
||||||
@ -299,28 +296,6 @@ describe('ssr: components', () => {
|
|||||||
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
|
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// wrap with fragment
|
|
||||||
expect(compile(`<suspense><div/></suspense>`).code)
|
|
||||||
.toMatchInlineSnapshot(`
|
|
||||||
"
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
|
||||||
_push(\`<div></div>\`)
|
|
||||||
}"
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('portal rendering', () => {
|
|
||||||
expect(compile(`<portal :target="target"><div/></portal>`).code)
|
|
||||||
.toMatchInlineSnapshot(`
|
|
||||||
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
|
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
|
||||||
_ssrRenderPortal((_push) => {
|
|
||||||
_push(\`<div></div>\`)
|
|
||||||
}, _ctx.target, _parent)
|
|
||||||
}"
|
|
||||||
`)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
16
packages/compiler-ssr/__tests__/ssrPortal.spec.ts
Normal file
16
packages/compiler-ssr/__tests__/ssrPortal.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { compile } from '../src'
|
||||||
|
|
||||||
|
describe('ssr compile: portal', () => {
|
||||||
|
test('should work', () => {
|
||||||
|
expect(compile(`<portal :target="target"><div/></portal>`).code)
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_ssrRenderPortal((_push) => {
|
||||||
|
_push(\`<div></div>\`)
|
||||||
|
}, _ctx.target, _parent)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
51
packages/compiler-ssr/__tests__/ssrSuspense.spec.ts
Normal file
51
packages/compiler-ssr/__tests__/ssrSuspense.spec.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { compile } from '../src'
|
||||||
|
|
||||||
|
describe('ssr compile: suspense', () => {
|
||||||
|
test('implicit default', () => {
|
||||||
|
expect(compile(`<suspense><foo/></suspense>`).code).toMatchInlineSnapshot(`
|
||||||
|
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
||||||
|
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
const _component_foo = _resolveComponent(\\"foo\\")
|
||||||
|
|
||||||
|
_push(_ssrRenderSuspense({
|
||||||
|
default: (_push) => {
|
||||||
|
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
|
||||||
|
},
|
||||||
|
_: 1
|
||||||
|
}))
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('explicit slots', () => {
|
||||||
|
expect(
|
||||||
|
compile(`<suspense>
|
||||||
|
<template #default>
|
||||||
|
<foo/>
|
||||||
|
</template>
|
||||||
|
<template #fallback>
|
||||||
|
loading...
|
||||||
|
</template>
|
||||||
|
</suspense>`).code
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
|
||||||
|
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
const _component_foo = _resolveComponent(\\"foo\\")
|
||||||
|
|
||||||
|
_push(_ssrRenderSuspense({
|
||||||
|
default: (_push) => {
|
||||||
|
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
|
||||||
|
},
|
||||||
|
fallback: (_push) => {
|
||||||
|
_push(\` loading... \`)
|
||||||
|
},
|
||||||
|
_: 1
|
||||||
|
}))
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
@ -14,6 +14,7 @@ export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
|
|||||||
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
|
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
|
||||||
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
|
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
|
||||||
export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`)
|
export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`)
|
||||||
|
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
|
||||||
|
|
||||||
export const ssrHelpers = {
|
export const ssrHelpers = {
|
||||||
[SSR_INTERPOLATE]: `ssrInterpolate`,
|
[SSR_INTERPOLATE]: `ssrInterpolate`,
|
||||||
@ -29,7 +30,8 @@ export const ssrHelpers = {
|
|||||||
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
|
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
|
||||||
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
|
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
|
||||||
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
|
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
|
||||||
[SSR_RENDER_PORTAL]: `ssrRenderPortal`
|
[SSR_RENDER_PORTAL]: `ssrRenderPortal`,
|
||||||
|
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: these are helpers imported from @vue/server-renderer
|
// Note: these are helpers imported from @vue/server-renderer
|
||||||
|
@ -30,17 +30,20 @@ import {
|
|||||||
traverseNode,
|
traverseNode,
|
||||||
ExpressionNode,
|
ExpressionNode,
|
||||||
TemplateNode,
|
TemplateNode,
|
||||||
findProp,
|
SUSPENSE
|
||||||
JSChildNode
|
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers'
|
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
|
||||||
import {
|
import {
|
||||||
SSRTransformContext,
|
SSRTransformContext,
|
||||||
processChildren,
|
processChildren,
|
||||||
processChildrenAsStatement
|
processChildrenAsStatement
|
||||||
} from '../ssrCodegenTransform'
|
} from '../ssrCodegenTransform'
|
||||||
|
import { ssrProcessPortal } from './ssrTransformPortal'
|
||||||
|
import {
|
||||||
|
ssrProcessSuspense,
|
||||||
|
ssrTransformSuspense
|
||||||
|
} from './ssrTransformSuspense'
|
||||||
import { isSymbol, isObject, isArray } from '@vue/shared'
|
import { isSymbol, isObject, isArray } from '@vue/shared'
|
||||||
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
|
|
||||||
|
|
||||||
// We need to construct the slot functions in the 1st pass to ensure proper
|
// We need to construct the slot functions in the 1st pass to ensure proper
|
||||||
// scope tracking, but the children of each slot cannot be processed until
|
// scope tracking, but the children of each slot cannot be processed until
|
||||||
@ -56,6 +59,12 @@ interface WIPSlotEntry {
|
|||||||
|
|
||||||
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
|
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
|
||||||
|
|
||||||
|
// ssr component transform is done in two phases:
|
||||||
|
// In phase 1. we use `buildSlot` to analyze the children of the component into
|
||||||
|
// WIP slot functions (it must be done in phase 1 because `buildSlot` relies on
|
||||||
|
// the core transform context).
|
||||||
|
// In phase 2. we convert the WIP slots from phase 1 into ssr-specific codegen
|
||||||
|
// nodes.
|
||||||
export const ssrTransformComponent: NodeTransform = (node, context) => {
|
export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||||
if (
|
if (
|
||||||
node.type !== NodeTypes.ELEMENT ||
|
node.type !== NodeTypes.ELEMENT ||
|
||||||
@ -67,6 +76,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
|||||||
const component = resolveComponentType(node, context, true /* ssr */)
|
const component = resolveComponentType(node, context, true /* ssr */)
|
||||||
if (isSymbol(component)) {
|
if (isSymbol(component)) {
|
||||||
componentTypeMap.set(node, component)
|
componentTypeMap.set(node, component)
|
||||||
|
if (component === SUSPENSE) {
|
||||||
|
return ssrTransformSuspense(node, context)
|
||||||
|
}
|
||||||
return // built-in component: fallthrough
|
return // built-in component: fallthrough
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,12 +144,15 @@ export function ssrProcessComponent(
|
|||||||
) {
|
) {
|
||||||
if (!node.ssrCodegenNode) {
|
if (!node.ssrCodegenNode) {
|
||||||
// this is a built-in component that fell-through.
|
// this is a built-in component that fell-through.
|
||||||
// just render its children.
|
|
||||||
const component = componentTypeMap.get(node)!
|
const component = componentTypeMap.get(node)!
|
||||||
if (component === PORTAL) {
|
if (component === PORTAL) {
|
||||||
return ssrProcessPortal(node, context)
|
return ssrProcessPortal(node, context)
|
||||||
|
} else if (component === SUSPENSE) {
|
||||||
|
return ssrProcessSuspense(node, context)
|
||||||
|
} else {
|
||||||
|
// real fall-through (e.g. KeepAlive): just render its children.
|
||||||
|
processChildren(node.children, context)
|
||||||
}
|
}
|
||||||
processChildren(node.children, context)
|
|
||||||
} else {
|
} else {
|
||||||
// finish up slot function expressions from the 1st pass.
|
// finish up slot function expressions from the 1st pass.
|
||||||
const wipEntries = wipMap.get(node) || []
|
const wipEntries = wipMap.get(node) || []
|
||||||
@ -161,47 +176,6 @@ export function ssrProcessComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ssrProcessPortal(node: ComponentNode, context: SSRTransformContext) {
|
|
||||||
const targetProp = findProp(node, 'target')
|
|
||||||
if (!targetProp) {
|
|
||||||
context.onError(
|
|
||||||
createSSRCompilerError(SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, node.loc)
|
|
||||||
)
|
|
||||||
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 {
|
|
||||||
context.onError(
|
|
||||||
createSSRCompilerError(
|
|
||||||
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
|
|
||||||
targetProp.loc
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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`
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
|
export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
|
||||||
|
|
||||||
const [baseNodeTransforms, baseDirectiveTransforms] = getBaseTransformPreset(
|
const [baseNodeTransforms, baseDirectiveTransforms] = getBaseTransformPreset(
|
||||||
|
60
packages/compiler-ssr/src/transforms/ssrTransformPortal.ts
Normal file
60
packages/compiler-ssr/src/transforms/ssrTransformPortal.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
ComponentNode,
|
||||||
|
findProp,
|
||||||
|
JSChildNode,
|
||||||
|
NodeTypes,
|
||||||
|
createSimpleExpression,
|
||||||
|
createFunctionExpression,
|
||||||
|
createCallExpression
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
import {
|
||||||
|
SSRTransformContext,
|
||||||
|
processChildrenAsStatement
|
||||||
|
} from '../ssrCodegenTransform'
|
||||||
|
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
|
||||||
|
import { SSR_RENDER_PORTAL } from '../runtimeHelpers'
|
||||||
|
|
||||||
|
// Note: this is a 2nd-pass codegen transform.
|
||||||
|
export function ssrProcessPortal(
|
||||||
|
node: ComponentNode,
|
||||||
|
context: SSRTransformContext
|
||||||
|
) {
|
||||||
|
const targetProp = findProp(node, 'target')
|
||||||
|
if (!targetProp) {
|
||||||
|
context.onError(
|
||||||
|
createSSRCompilerError(SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, node.loc)
|
||||||
|
)
|
||||||
|
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 {
|
||||||
|
context.onError(
|
||||||
|
createSSRCompilerError(
|
||||||
|
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
|
||||||
|
targetProp.loc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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`
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
78
packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
Normal file
78
packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
ComponentNode,
|
||||||
|
TransformContext,
|
||||||
|
buildSlots,
|
||||||
|
createFunctionExpression,
|
||||||
|
FunctionExpression,
|
||||||
|
TemplateChildNode,
|
||||||
|
createCallExpression,
|
||||||
|
SlotsExpression
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
import {
|
||||||
|
SSRTransformContext,
|
||||||
|
processChildrenAsStatement
|
||||||
|
} from '../ssrCodegenTransform'
|
||||||
|
import { SSR_RENDER_SUSPENSE } from '../runtimeHelpers'
|
||||||
|
|
||||||
|
const wipMap = new WeakMap<ComponentNode, WIPEntry>()
|
||||||
|
|
||||||
|
interface WIPEntry {
|
||||||
|
slotsExp: SlotsExpression
|
||||||
|
wipSlots: Array<{
|
||||||
|
fn: FunctionExpression
|
||||||
|
children: TemplateChildNode[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase 1
|
||||||
|
export function ssrTransformSuspense(
|
||||||
|
node: ComponentNode,
|
||||||
|
context: TransformContext
|
||||||
|
) {
|
||||||
|
return () => {
|
||||||
|
if (node.children.length) {
|
||||||
|
const wipEntry: WIPEntry = {
|
||||||
|
slotsExp: null as any,
|
||||||
|
wipSlots: []
|
||||||
|
}
|
||||||
|
wipMap.set(node, wipEntry)
|
||||||
|
wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => {
|
||||||
|
const fn = createFunctionExpression(
|
||||||
|
[`_push`],
|
||||||
|
undefined, // no return, assign body later
|
||||||
|
true, // newline
|
||||||
|
false, // suspense slots are not treated as normal slots
|
||||||
|
loc
|
||||||
|
)
|
||||||
|
wipEntry.wipSlots.push({
|
||||||
|
fn,
|
||||||
|
children
|
||||||
|
})
|
||||||
|
return fn
|
||||||
|
}).slots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase 2
|
||||||
|
export function ssrProcessSuspense(
|
||||||
|
node: ComponentNode,
|
||||||
|
context: SSRTransformContext
|
||||||
|
) {
|
||||||
|
// complete wip slots with ssr code
|
||||||
|
const wipEntry = wipMap.get(node)
|
||||||
|
if (!wipEntry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { slotsExp, wipSlots } = wipEntry
|
||||||
|
for (let i = 0; i < wipSlots.length; i++) {
|
||||||
|
const { fn, children } = wipSlots[i]
|
||||||
|
fn.body = processChildrenAsStatement(children, context)
|
||||||
|
}
|
||||||
|
// _push(ssrRenderSuspense(slots))
|
||||||
|
context.pushStatement(
|
||||||
|
createCallExpression(`_push`, [
|
||||||
|
createCallExpression(context.helper(SSR_RENDER_SUSPENSE), [slotsExp])
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import { createApp, h, Suspense } from 'vue'
|
import { createApp, h, Suspense } from 'vue'
|
||||||
import { renderToString } from '../src/renderToString'
|
import { renderToString } from '../src/renderToString'
|
||||||
|
import { ssrRenderSuspense } from '../src/helpers/ssrRenderSuspense'
|
||||||
|
import { ssrRenderComponent } from '../src'
|
||||||
|
|
||||||
describe('SSR Suspense', () => {
|
describe('SSR Suspense', () => {
|
||||||
let logError: jest.SpyInstance
|
let logError: jest.SpyInstance
|
||||||
@ -24,103 +26,163 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('render', async () => {
|
describe('compiled', () => {
|
||||||
const Comp = {
|
test('basic', async () => {
|
||||||
render() {
|
const app = createApp({
|
||||||
return h(Suspense, null, {
|
ssrRender(_ctx, _push) {
|
||||||
default: h(ResolvingAsync),
|
_push(
|
||||||
fallback: h('div', 'fallback')
|
ssrRenderSuspense({
|
||||||
})
|
default: _push => {
|
||||||
}
|
_push('<div>async</div>')
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
|
|
||||||
expect(logError).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('fallback', async () => {
|
|
||||||
const Comp = {
|
|
||||||
render() {
|
|
||||||
return h(Suspense, null, {
|
|
||||||
default: h(RejectingAsync),
|
|
||||||
fallback: h('div', 'fallback')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
|
||||||
expect(logError).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('2 components', async () => {
|
|
||||||
const Comp = {
|
|
||||||
render() {
|
|
||||||
return h(Suspense, null, {
|
|
||||||
default: h('div', [h(ResolvingAsync), h(ResolvingAsync)]),
|
|
||||||
fallback: h('div', 'fallback')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(
|
|
||||||
`<div><div>async</div><div>async</div></div>`
|
|
||||||
)
|
|
||||||
expect(logError).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('resolving component + rejecting component', async () => {
|
|
||||||
const Comp = {
|
|
||||||
render() {
|
|
||||||
return h(Suspense, null, {
|
|
||||||
default: h('div', [h(ResolvingAsync), h(RejectingAsync)]),
|
|
||||||
fallback: h('div', 'fallback')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
|
||||||
expect(logError).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('failing suspense in passing suspense', async () => {
|
|
||||||
const Comp = {
|
|
||||||
render() {
|
|
||||||
return h(Suspense, null, {
|
|
||||||
default: h('div', [
|
|
||||||
h(ResolvingAsync),
|
|
||||||
h(Suspense, null, {
|
|
||||||
default: h('div', [h(RejectingAsync)]),
|
|
||||||
fallback: h('div', 'fallback 2')
|
|
||||||
})
|
})
|
||||||
]),
|
)
|
||||||
fallback: h('div', 'fallback 1')
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(
|
expect(await renderToString(app)).toBe(`<div>async</div>`)
|
||||||
`<div><div>async</div><div>fallback 2</div></div>`
|
expect(logError).not.toHaveBeenCalled()
|
||||||
)
|
})
|
||||||
expect(logError).toHaveBeenCalled()
|
|
||||||
|
test('with async component', async () => {
|
||||||
|
const app = createApp({
|
||||||
|
ssrRender(_ctx, _push) {
|
||||||
|
_push(
|
||||||
|
ssrRenderSuspense({
|
||||||
|
default: _push => {
|
||||||
|
_push(ssrRenderComponent(ResolvingAsync))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await renderToString(app)).toBe(`<div>async</div>`)
|
||||||
|
expect(logError).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fallback', async () => {
|
||||||
|
const app = createApp({
|
||||||
|
ssrRender(_ctx, _push) {
|
||||||
|
_push(
|
||||||
|
ssrRenderSuspense({
|
||||||
|
default: _push => {
|
||||||
|
_push(ssrRenderComponent(RejectingAsync))
|
||||||
|
},
|
||||||
|
fallback: _push => {
|
||||||
|
_push('<div>fallback</div>')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await renderToString(app)).toBe(`<div>fallback</div>`)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('passing suspense in failing suspense', async () => {
|
describe('vnode', () => {
|
||||||
const Comp = {
|
test('content', async () => {
|
||||||
render() {
|
const Comp = {
|
||||||
return h(Suspense, null, {
|
render() {
|
||||||
default: h('div', [
|
return h(Suspense, null, {
|
||||||
h(RejectingAsync),
|
default: h(ResolvingAsync),
|
||||||
h(Suspense, null, {
|
fallback: h('div', 'fallback')
|
||||||
default: h('div', [h(ResolvingAsync)]),
|
})
|
||||||
fallback: h('div', 'fallback 2')
|
}
|
||||||
})
|
|
||||||
]),
|
|
||||||
fallback: h('div', 'fallback 1')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback 1</div>`)
|
expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
|
||||||
expect(logError).toHaveBeenCalled()
|
expect(logError).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fallback', async () => {
|
||||||
|
const Comp = {
|
||||||
|
render() {
|
||||||
|
return h(Suspense, null, {
|
||||||
|
default: h(RejectingAsync),
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('2 components', async () => {
|
||||||
|
const Comp = {
|
||||||
|
render() {
|
||||||
|
return h(Suspense, null, {
|
||||||
|
default: h('div', [h(ResolvingAsync), h(ResolvingAsync)]),
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await renderToString(createApp(Comp))).toBe(
|
||||||
|
`<div><div>async</div><div>async</div></div>`
|
||||||
|
)
|
||||||
|
expect(logError).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolving component + rejecting component', async () => {
|
||||||
|
const Comp = {
|
||||||
|
render() {
|
||||||
|
return h(Suspense, null, {
|
||||||
|
default: h('div', [h(ResolvingAsync), h(RejectingAsync)]),
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('failing suspense in passing suspense', async () => {
|
||||||
|
const Comp = {
|
||||||
|
render() {
|
||||||
|
return h(Suspense, null, {
|
||||||
|
default: h('div', [
|
||||||
|
h(ResolvingAsync),
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: h('div', [h(RejectingAsync)]),
|
||||||
|
fallback: h('div', 'fallback 2')
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
fallback: h('div', 'fallback 1')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await renderToString(createApp(Comp))).toBe(
|
||||||
|
`<div><div>async</div><div>fallback 2</div></div>`
|
||||||
|
)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('passing suspense in failing suspense', async () => {
|
||||||
|
const Comp = {
|
||||||
|
render() {
|
||||||
|
return h(Suspense, null, {
|
||||||
|
default: h('div', [
|
||||||
|
h(RejectingAsync),
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: h('div', [h(ResolvingAsync)]),
|
||||||
|
fallback: h('div', 'fallback 2')
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
fallback: h('div', 'fallback 1')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await renderToString(createApp(Comp))).toBe(
|
||||||
|
`<div>fallback 1</div>`
|
||||||
|
)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
19
packages/server-renderer/src/helpers/ssrRenderSuspense.ts
Normal file
19
packages/server-renderer/src/helpers/ssrRenderSuspense.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString'
|
||||||
|
import { NOOP } from '@vue/shared'
|
||||||
|
|
||||||
|
type ContentRenderFn = (push: PushFn) => void
|
||||||
|
|
||||||
|
export async function ssrRenderSuspense({
|
||||||
|
default: renderContent = NOOP,
|
||||||
|
fallback: renderFallback = NOOP
|
||||||
|
}: Record<string, ContentRenderFn | undefined>): Promise<ResolvedSSRBuffer> {
|
||||||
|
try {
|
||||||
|
const { push, getBuffer } = createBuffer()
|
||||||
|
renderContent(push)
|
||||||
|
return await getBuffer()
|
||||||
|
} catch {
|
||||||
|
const { push, getBuffer } = createBuffer()
|
||||||
|
renderFallback(push)
|
||||||
|
return getBuffer()
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ export {
|
|||||||
export { ssrInterpolate } from './helpers/ssrInterpolate'
|
export { ssrInterpolate } from './helpers/ssrInterpolate'
|
||||||
export { ssrRenderList } from './helpers/ssrRenderList'
|
export { ssrRenderList } from './helpers/ssrRenderList'
|
||||||
export { ssrRenderPortal } from './helpers/ssrRenderPortal'
|
export { ssrRenderPortal } from './helpers/ssrRenderPortal'
|
||||||
|
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
|
||||||
|
|
||||||
// v-model helpers
|
// v-model helpers
|
||||||
export {
|
export {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user