create-bundle-renderer.js 4.09 KB
Newer Older
liang ce committed
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
/* @flow */

import { createPromiseCallback } from '../util'
import { createBundleRunner } from './create-bundle-runner'
import type { Renderer, RenderOptions } from '../create-renderer'
import { createSourceMapConsumers, rewriteErrorTrace } from './source-map-support'

const fs = require('fs')
const path = require('path')
const PassThrough = require('stream').PassThrough

const INVALID_MSG =
  'Invalid server-rendering bundle format. Should be a string ' +
  'or a bundle Object of type:\n\n' +
`{
  entry: string;
  files: { [filename: string]: string; };
  maps: { [filename: string]: string; };
}\n`

// The render bundle can either be a string (single bundled file)
// or a bundle manifest object generated by vue-ssr-webpack-plugin.
type RenderBundle = {
  basedir?: string;
  entry: string;
  files: { [filename: string]: string; };
  maps: { [filename: string]: string; };
  modules?: { [filename: string]: Array<string> };
};

export function createBundleRendererCreator (
  createRenderer: (options?: RenderOptions) => Renderer
) {
  return function createBundleRenderer (
    bundle: string | RenderBundle,
    rendererOptions?: RenderOptions = {}
  ) {
    let files, entry, maps
    let basedir = rendererOptions.basedir

    // load bundle if given filepath
    if (
      typeof bundle === 'string' &&
      /\.js(on)?$/.test(bundle) &&
      path.isAbsolute(bundle)
    ) {
      if (fs.existsSync(bundle)) {
        const isJSON = /\.json$/.test(bundle)
        basedir = basedir || path.dirname(bundle)
        bundle = fs.readFileSync(bundle, 'utf-8')
        if (isJSON) {
          try {
            bundle = JSON.parse(bundle)
          } catch (e) {
            throw new Error(`Invalid JSON bundle file: ${bundle}`)
          }
        }
      } else {
        throw new Error(`Cannot locate bundle file: ${bundle}`)
      }
    }

    if (typeof bundle === 'object') {
      entry = bundle.entry
      files = bundle.files
      basedir = basedir || bundle.basedir
      maps = createSourceMapConsumers(bundle.maps)
      if (typeof entry !== 'string' || typeof files !== 'object') {
        throw new Error(INVALID_MSG)
      }
    } else if (typeof bundle === 'string') {
      entry = '__vue_ssr_bundle__'
      files = { '__vue_ssr_bundle__': bundle }
      maps = {}
    } else {
      throw new Error(INVALID_MSG)
    }

    const renderer = createRenderer(rendererOptions)

    const run = createBundleRunner(
      entry,
      files,
      basedir,
      rendererOptions.runInNewContext
    )

    return {
      renderToString: (context?: Object, cb: any) => {
        if (typeof context === 'function') {
          cb = context
          context = {}
        }

        let promise
        if (!cb) {
          ({ promise, cb } = createPromiseCallback())
        }

        run(context).catch(err => {
          rewriteErrorTrace(err, maps)
          cb(err)
        }).then(app => {
          if (app) {
            renderer.renderToString(app, context, (err, res) => {
              rewriteErrorTrace(err, maps)
              cb(err, res)
            })
          }
        })

        return promise
      },

      renderToStream: (context?: Object) => {
        const res = new PassThrough()
        run(context).catch(err => {
          rewriteErrorTrace(err, maps)
          // avoid emitting synchronously before user can
          // attach error listener
          process.nextTick(() => {
            res.emit('error', err)
          })
        }).then(app => {
          if (app) {
            const renderStream = renderer.renderToStream(app, context)

            renderStream.on('error', err => {
              rewriteErrorTrace(err, maps)
              res.emit('error', err)
            })

            // relay HTMLStream special events
            if (rendererOptions && rendererOptions.template) {
              renderStream.on('beforeStart', () => {
                res.emit('beforeStart')
              })
              renderStream.on('beforeEnd', () => {
                res.emit('beforeEnd')
              })
            }

            renderStream.pipe(res)
          }
        })

        return res
      }
    }
  }
}