]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(ssr): compiler-ssr support for Suspense
authorEvan You <yyx990803@gmail.com>
Tue, 10 Mar 2020 20:52:08 +0000 (16:52 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 10 Mar 2020 20:52:31 +0000 (16:52 -0400)
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrPortal.spec.ts [new file with mode: 0644]
packages/compiler-ssr/__tests__/ssrSuspense.spec.ts [new file with mode: 0644]
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformPortal.ts [new file with mode: 0644]
packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts [new file with mode: 0644]
packages/server-renderer/__tests__/ssrSuspense.spec.ts
packages/server-renderer/src/helpers/ssrRenderSuspense.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts

index 4414ad3a89110c2db880031c154eaaf536e748da..89e4b1d8d977d37986bd2f2405dc67bfaf9e4665 100644 (file)
@@ -34,7 +34,7 @@ describe('ssr: components', () => {
       .toMatchInlineSnapshot(`
       "const { resolveDynamicComponent: _resolveDynamicComponent } = require(\\"vue\\")
       const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
-      
+
       return function ssrRender(_ctx, _push, _parent) {
         _push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo, _ctx.$), { prop: \\"b\\" }, null, _parent))
       }"
@@ -269,7 +269,6 @@ describe('ssr: components', () => {
     })
 
     test('built-in fallthroughs', () => {
-      // no fragment
       expect(compile(`<transition><div/></transition>`).code)
         .toMatchInlineSnapshot(`
         "
@@ -278,7 +277,6 @@ describe('ssr: components', () => {
         }"
       `)
 
-      // wrap with fragment
       expect(compile(`<transition-group><div/></transition-group>`).code)
         .toMatchInlineSnapshot(`
         "
@@ -287,7 +285,6 @@ describe('ssr: components', () => {
         }"
       `)
 
-      // no fragment
       expect(compile(`<keep-alive><foo/></keep-alive>`).code)
         .toMatchInlineSnapshot(`
         "const { resolveComponent: _resolveComponent } = require(\\"vue\\")
@@ -299,28 +296,6 @@ describe('ssr: components', () => {
           _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)
-        }"
-      `)
     })
   })
 })
diff --git a/packages/compiler-ssr/__tests__/ssrPortal.spec.ts b/packages/compiler-ssr/__tests__/ssrPortal.spec.ts
new file mode 100644 (file)
index 0000000..5490649
--- /dev/null
@@ -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)
+      }"
+    `)
+  })
+})
diff --git a/packages/compiler-ssr/__tests__/ssrSuspense.spec.ts b/packages/compiler-ssr/__tests__/ssrSuspense.spec.ts
new file mode 100644 (file)
index 0000000..7f38f51
--- /dev/null
@@ -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
+        }))
+      }"
+    `)
+  })
+})
index 01875cf05caf5860f00173dbed313227c7a57ea8..bfd8370891aca9da80809264ff1c7d59b00290c3 100644 (file)
@@ -14,6 +14,7 @@ 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 SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
 
 export const ssrHelpers = {
   [SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -29,7 +30,8 @@ export const ssrHelpers = {
   [SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
   [SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
   [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
index 0e3f1457bf4c6cd40e5a9fc1cc9bb5f01c86b8e7..1acf45ee5b827b00a4cab81732e678390f446524 100644 (file)
@@ -30,17 +30,20 @@ import {
   traverseNode,
   ExpressionNode,
   TemplateNode,
-  findProp,
-  JSChildNode
+  SUSPENSE
 } from '@vue/compiler-dom'
-import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers'
+import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
 import {
   SSRTransformContext,
   processChildren,
   processChildrenAsStatement
 } from '../ssrCodegenTransform'
+import { ssrProcessPortal } from './ssrTransformPortal'
+import {
+  ssrProcessSuspense,
+  ssrTransformSuspense
+} from './ssrTransformSuspense'
 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
 // scope tracking, but the children of each slot cannot be processed until
@@ -56,6 +59,12 @@ interface WIPSlotEntry {
 
 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) => {
   if (
     node.type !== NodeTypes.ELEMENT ||
@@ -67,6 +76,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
   const component = resolveComponentType(node, context, true /* ssr */)
   if (isSymbol(component)) {
     componentTypeMap.set(node, component)
+    if (component === SUSPENSE) {
+      return ssrTransformSuspense(node, context)
+    }
     return // built-in component: fallthrough
   }
 
@@ -132,12 +144,15 @@ export function ssrProcessComponent(
 ) {
   if (!node.ssrCodegenNode) {
     // this is a built-in component that fell-through.
-    // just render its children.
     const component = componentTypeMap.get(node)!
     if (component === PORTAL) {
       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 {
     // finish up slot function expressions from the 1st pass.
     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>()
 
 const [baseNodeTransforms, baseDirectiveTransforms] = getBaseTransformPreset(
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts b/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts
new file mode 100644 (file)
index 0000000..c380e67
--- /dev/null
@@ -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`
+    ])
+  )
+}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
new file mode 100644 (file)
index 0000000..2a948d6
--- /dev/null
@@ -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])
+    ])
+  )
+}
index c4c3b2d5a9fe62c8e0b90ec847bcf79f31323026..893c69efab8830db0cc4ac3b8ef12478473fbad0 100644 (file)
@@ -1,5 +1,7 @@
 import { createApp, h, Suspense } from 'vue'
 import { renderToString } from '../src/renderToString'
+import { ssrRenderSuspense } from '../src/helpers/ssrRenderSuspense'
+import { ssrRenderComponent } from '../src'
 
 describe('SSR Suspense', () => {
   let logError: jest.SpyInstance
@@ -24,103 +26,163 @@ describe('SSR Suspense', () => {
     }
   }
 
-  test('render', async () => {
-    const Comp = {
-      render() {
-        return h(Suspense, null, {
-          default: h(ResolvingAsync),
-          fallback: h('div', 'fallback')
-        })
-      }
-    }
+  describe('compiled', () => {
+    test('basic', async () => {
+      const app = createApp({
+        ssrRender(_ctx, _push) {
+          _push(
+            ssrRenderSuspense({
+              default: _push => {
+                _push('<div>async</div>')
+              }
+            })
+          )
+        }
+      })
+
+      expect(await renderToString(app)).toBe(`<div>async</div>`)
+      expect(logError).not.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(createApp(Comp))).toBe(`<div>async</div>`)
-    expect(logError).not.toHaveBeenCalled()
+      expect(await renderToString(app)).toBe(`<div>fallback</div>`)
+      expect(logError).toHaveBeenCalled()
+    })
   })
 
-  test('fallback', async () => {
-    const Comp = {
-      render() {
-        return h(Suspense, null, {
-          default: h(RejectingAsync),
-          fallback: h('div', 'fallback')
-        })
+  describe('vnode', () => {
+    test('content', async () => {
+      const Comp = {
+        render() {
+          return h(Suspense, null, {
+            default: h(ResolvingAsync),
+            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>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><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('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>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>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><div>async</div><div>fallback 2</div></div>`
-    )
-    expect(logError).toHaveBeenCalled()
-  })
+      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')
+          })
+        }
+      }
 
-  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><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()
+      expect(await renderToString(createApp(Comp))).toBe(
+        `<div>fallback 1</div>`
+      )
+      expect(logError).toHaveBeenCalled()
+    })
   })
 })
diff --git a/packages/server-renderer/src/helpers/ssrRenderSuspense.ts b/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
new file mode 100644 (file)
index 0000000..efb4bcd
--- /dev/null
@@ -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()
+  }
+}
index 315b3ae18c71dd9e8d3d62da59d9cb6665d637b9..48915c63f61ecb79627b96f32f7909f176bd959b 100644 (file)
@@ -14,6 +14,7 @@ export {
 export { ssrInterpolate } from './helpers/ssrInterpolate'
 export { ssrRenderList } from './helpers/ssrRenderList'
 export { ssrRenderPortal } from './helpers/ssrRenderPortal'
+export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
 
 // v-model helpers
 export {