vue-server-renderer not able to build bundle file

2.4k views Asked by At

I'm trying to create a @vue/cli3 SSR project and cant seem to generate the server bundle files. Client builds fine.

package.json scripts

"scripts": {
    "serve": "npm run build && node scripts/serve",
    "build": "npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json",
    "build:client": "vue-cli-service build",
    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
}

Error from running yarn serve

$ yarn serve
yarn run v1.9.2
$ npm run build && node scripts/serve

> [email protected] build /path/to/file/project-name
> npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json


> [email protected] build:server /path/to/file/project-name
> cross-env WEBPACK_TARGET=node vue-cli-service build


⠙  Building for production...(node:24514) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead
⠏  Building for production...(node:24514) UnhandledPromiseRejectionWarning: Error: Server-side bundle should have one single entry file. Avoid using CommonsChunkPlugin in the server config.
    at /path/to/file/project-name/node_modules/vue-server-renderer/server-plugin.js:58:13
    at _err2 (eval at create (/path/to/file/project-name/node_modules/tapable/lib/HookCodeFactory.js:24:12), <anonymous>:22:1)
    at callback (/path/to/file/project-name/node_modules/copy-webpack-plugin/dist/index.js:77:17)
    at /path/to/file/project-name/node_modules/copy-webpack-plugin/dist/index.js:118:24
    at <anonymous>
(node:24514) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:24514) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

vue.config.js

const path = require('path')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')

const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'

const resolve = (file) => path.resolve(__dirname, file)

const target = TARGET_NODE
  ? 'server'
  : 'client'

module.exports = {
  configureWebpack: () => ({
    entry: {
      app: `./src/entry-${target}`
    },
    target: TARGET_NODE ? 'node' : 'web',
    node: TARGET_NODE ? undefined : false,
    plugins: [
      TARGET_NODE
        ? new VueSSRServerPlugin()
        : new VueSSRClientPlugin()
    ],
    externals: TARGET_NODE ? nodeExternals({
      whitelist: /\.css$/
    }) : undefined,
    output: {
      libraryTarget: TARGET_NODE
        ? 'commonjs2'
        : undefined
    },
    optimization: {
      splitChunks: undefined
    },
    resolve:{
      alias: {
        '@': resolve('src'),
        'public': resolve('public')
      }
    }
  }),
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options =>
        merge(options, {
          optimizeSSR: false
        })
      )
  }
}

And lastly, the server.js file:

/* eslint-disable no-console */

const fs = require('fs')
const path = require('path')
const express = require('express')
const compression = require('compression')
const favicon = require('serve-favicon')
const microcache = require('route-cache')
const Ouch = require('ouch')
var proxy = require('http-proxy-middleware')
const { createBundleRenderer } = require('vue-server-renderer')

const resolve = file => path.resolve(__dirname, file)

const devServerBaseURL = process.env.DEV_SERVER_BASE_URL || 'http://localhost'
const devServerPort = process.env.DEV_SERVER_PORT || 8080
const isProd = process.env.NODE_ENV === 'production'
const useMicroCache = process.env.MICRO_CACHE !== 'false'

const serverInfo =
  `express/${require('express/package.json').version} ` +
  `vue-server-renderer/${require('vue-server-renderer/package.json').version}`

const app = express()

function createRenderer (bundle, options) {
  return createBundleRenderer(bundle, Object.assign(options, {
    runInNewContext: false
  }))
}

let renderer
const templatePath = path.resolve(__dirname, './src/index.template.html')

const bundle = require('./dist/vue-ssr-server-bundle.json')
const template = fs.readFileSync(templatePath, 'utf-8')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
renderer = createRenderer(bundle, {
  template,
  clientManifest
})

if (process.env.NODE_ENV !== 'production') {
  app.use('/js/main*', proxy({
    target: `${devServerBaseURL}/${devServerPort}`,
    changeOrigin: true,
    pathRewrite: function (path) {
      return path.includes('main')
        ? '/main.js'
        : path
    },
    prependPath: false
  }))

  app.use('/*hot-update*', proxy({
    target: `${devServerBaseURL}/${devServerPort}`,
    changeOrigin: true
  }))

  app.use('/sockjs-node', proxy({
    target: `${devServerBaseURL}/${devServerPort}`,
    changeOrigin: true,
    ws: true
  }))
}

const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})

app.use('/js', express.static(path.resolve(__dirname, './dist/js')))
app.use('/css', express.static(path.resolve(__dirname, './dist/css')))
app.use(express.json())
app.use(compression({ threshold: 0 }))
app.use(favicon('./public/favicon.ico'))
app.use('/public/manifest.json', serve('./manifest.json', true))
app.use('/public', serve('./public', true))
app.use('/public/robots.txt', serve('./robots.txt'))

app.get('/sitemap.xml', (req, res) => {
  res.setHeader('Content-Type', 'text/xml')
  res.sendFile(resolve('./public/sitemap.xml'))
})

// since this app has no user-specific content, every page is micro-cacheable.
// if your app involves user-specific content, you need to implement custom
// logic to determine whether a request is cacheable based on its url and
// headers.
// 10-minute microcache.
// https://www.nginx.com/blog/benefits-of-microcaching-nginx/
const cacheMiddleware = microcache.cacheSeconds(10 * 60, req => useMicroCache && req.originalUrl)

const ouchInstance = (new Ouch()).pushHandler(new Ouch.handlers.PrettyPageHandler('orange', null, 'sublime'))

function render (req, res) {
  const start = Date.now()

  res.setHeader('Content-Type', 'text/html')
  res.setHeader('Server', serverInfo)

  const context = {
    url: req.url,
    res
  }

  const handleError = err => {
    if (err.url) {
      res.redirect(err.url)
    } else if (err.code === 404) {
      res.status(404).send('404 | Page Not Found')
    } else {
      ouchInstance.handleException(err, req, res, output => {
        console.log(JSON.stringify(err))
        console.log('Error handled!')
      })
    }
  }

  renderer.renderToString(context, (err, html) => {
    if (err) return handleError(err)

    res.end(html)

    !isProd && console.log(`whole request: ${Date.now() - start}ms`)
  })
}

app.get('*', render, cacheMiddleware)

const port = process.env.PORT || 8095
const host = process.env.HOST || '0.0.0.0'
app.listen(port, host, () => {
  console.log(`server started at ${host}:${port}`)
})

It seems that this error comes from vue-server-renderer, and is related somehow to https://github.com/vuejs/vue/issues/5553 but it doesnt need to give any clues as to how to resolve this issue. All promises have catch blocks, so no rejections should be unhandled. This only occurs when WEBPACK_TARGET === 'node' and therefore VueSSRServerPlugin is used.

1

There are 1 answers

0
alexandernst On

I solved it like this:

const base = require("@vue/cli-service/webpack.config");
delete base.optimization;

module.exports = merge(base, {
   ... config related to SSR ...
});