All files / server-renderer/src renderToString.ts

97.14% Statements 68/70
100% Branches 30/30
100% Functions 12/12
96.77% Lines 60/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152                      16x             688x 382x     306x 306x 472x 472x 194x 194x     278x 110x 102x 102x       168x 168x 64x 64x 64x       104x     298x       354x       555x 555x 657x 657x 484x     173x     555x             431x   87x       344x 344x   344x     344x 344x 339x 331x   13x 13x   331x 331x       554x 554x 24x 18x                             31x 31x 31x   31x 31x 31x               38x 20x 76x 76x 56x           492x 15x 15x     15x            
import {
  type App,
  type VNode,
  createApp,
  createVNode,
  ssrContextKey,
  ssrUtils,
} from '@vue/runtime-dom'
import { NOOP, isPromise, isString } from '@vue/shared'
import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
 
const { isVNode } = ssrUtils
 
function nestedUnrollBuffer(
  buffer: SSRBuffer,
  parentRet: string,
  startIndex: number,
): Promise<string> | string {
  if (!buffer.hasAsync) {
    return parentRet + unrollBufferSync(buffer)
  }
 
  let ret = parentRet
  for (let i = startIndex; i < buffer.length; i += 1) {
    const item = buffer[i]
    if (isString(item)) {
      ret += item
      continue
    }
 
    if (isPromise(item)) {
      return item.then(nestedItem => {
        buffer[i] = nestedItem
        return nestedUnrollBuffer(buffer, ret, i)
      })
    }
 
    const result = nestedUnrollBuffer(item, ret, 0)
    if (isPromise(result)) {
      return result.then(nestedItem => {
        buffer[i] = nestedItem
        return nestedUnrollBuffer(buffer, '', i)
      })
    }
 
    ret = result
  }
 
  return ret
}
 
export function unrollBuffer(buffer: SSRBuffer): Promise<string> | string {
  return nestedUnrollBuffer(buffer, '', 0)
}
 
function unrollBufferSync(buffer: SSRBuffer): string {
  let ret = ''
  for (let i = 0; i < buffer.length; i++) {
    let item = buffer[i]
    if (isString(item)) {
      ret += item
    } else {
      // since this is a sync buffer, child buffers are never promises
      ret += unrollBufferSync(item as SSRBuffer)
    }
  }
  return ret
}
 
export async function renderToString(
  input: App | VNode,
  context: SSRContext = {},
): Promise<string> {
  if (isVNode(input)) {
    // raw vnode, wrap with app (for context)
    return renderToString(createApp({ render: () => input }), context)
  }
 
  // rendering an app
  const vnode = createVNode(input._component, input._props)
  vnode.appContext = input._context
  // provide the ssr context to the tree
  input.provide(ssrContextKey, context)
  let buffer: SSRBuffer | undefined
  let result: string
  try {
    buffer = await renderComponentVNode(vnode)
    result = await unrollBuffer(buffer)
    await resolveTeleports(context)
  } catch (err) {
    cleanupFailedRender(buffer, context)
    throw err
  }
  stopWatchers(context)
  return result
}
 
export function stopWatchers(context: SSRContext, start = 0): void {
  const handles = context.__watcherHandles
  if (handles) {
    for (let i = start; i < handles.length; i++) {
      handles[i]()
    }
  }
}
 
/**
 * Stops watchers without masking the render error. Components after the
 * failure point are still in flight and can register watchers once the render
 * has settled, so those are stopped when they finish.
 * Cleanup errors here have already been reported by the watcher.
 */
export function cleanupFailedRender(
  buffer: SSRBuffer | undefined,
  context: SSRContext,
): void {
  const stopped = context.__watcherHandles ? context.__watcherHandles.length : 0
  try {
    stopWatchers(context)
  } catch {}
  ;(async () => {
    if (buffer) await settleBuffer(buffer)
    for (const key in context.__teleportBuffers) {
      await settleBuffer(context.__teleportBuffers[key])
    }
    stopWatchers(context, stopped)
  })().catch(NOOP)
}
 
async function settleBuffer(buffer: SSRBuffer): Promise<void> {
  if (!buffer.hasAsync) return
  for (let item of buffer) {
    try {
      if (isPromise(item)) item = await item
      if (!isString(item)) await settleBuffer(item)
    } catch {}
  }
}
 
export async function resolveTeleports(context: SSRContext): Promise<void> {
  if (context.__teleportBuffers) {
    context.teleports = context.teleports || {}
    for (const key in context.__teleportBuffers) {
      // note: it's OK to await sequentially here because the Promises were
      // created eagerly in parallel.
      context.teleports[key] = await unrollBuffer(
        context.__teleportBuffers[key],
      )
    }
  }
}