60frames / webpack-hot-server-middleware

:fire: Hot reload webpack bundles on the server
MIT License
324 stars 50 forks source link

TypeError: serverRenderer is not a function #7

Closed ghost closed 7 years ago

ghost commented 7 years ago

I don't get the error until I try to load the page:

server.jsx:

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { match } from 'react-router/es6';
import { ServerRoot } from 'Root';
import configureStore from 'store';
import cheerio from 'cheerio';

module.exports = function(indexHTML) {
    return function (req, res) {
        var routes = require('./routes.jsx');
        var store = configureStore();

        var css = [];

        match({
            routes: routes.default,
            location: req.originalUrl
        }, (error, redirectLocation, renderProps) => {
            if (error) {
                res.status(500).send(error.message)
            } else if (redirectLocation) {
                res.redirect(302, redirectLocation.pathname + redirectLocation.search)
            } else if (renderProps) {
                var body = ReactDOMServer.renderToString(
                    <ServerRoot store={store} renderProps={renderProps} onInsertCss={(...styles) => {
                        styles.forEach(style => css.push(style._getCss()));
                    }}/>
                );

                const state = store.getState();

                var $ = cheerio.load(indexHTML);

                $('#server-style').html(css.join(''));
                $('#redux-state').html('window.__REDUX_STATE__ = ' + JSON.stringify(state).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--') + ';');
                $('#app').html('<div>' + body + '</div>');

                res.set('content-type', 'text/html');
                res.send($.html());
                res.end();
            } else {
                res.status(404).send('Not found')
            }
        });
    }
}

app.js:

...
var webpack = require('webpack');
var clientConfig = require(path.join(__dirname, '/config/webpack.config.client.js'));
var serverConfig = require(path.join(__dirname, '/config/webpack.config.server.js'));
var devServerConfig = clientConfig.devServer;

app.use(serverConfig.output.publicPath, express.static(serverConfig.output.path));

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public/favicons')));

if (process.env.NODE_ENV === 'development') {
    var compiler = webpack(clientConfig);

    var serverCompiler = webpack([clientConfig, serverConfig]);

    const webpackDevMiddlewareInstance = require('webpack-dev-middleware')(compiler, devServerConfig);
    const webpackHotServerMiddlewareInstance = require('webpack-hot-server-middleware')(serverCompiler);

    app.use(webpackDevMiddlewareInstance);
    app.use(webpackHotServerMiddlewareInstance);

    if (devServerConfig.hot) {
        app.use(require('webpack-hot-middleware')(compiler, {
            log: console.log,
            path: '/__webpack_hmr',
            heartbeat: 10000
        }));
    }

    webpackDevMiddlewareInstance.waitUntilValid(function () {
        compiler.outputFileSystem.readFile(path.join(compiler.outputPath, 'index.html'), function (err, result) {
            if (err) throw err;

            ready(result);
        });
    });
} else
    ready(fs.readFileSync(path.join(__dirname, 'builds/client/index.html')));

function ready(indexHTML) {
    app.use('*', require('./builds/server/server.js')(indexHTML));
    ...
}

I may be doing something very wrong or incorrectly since I am still relatively new to all of this.

Thanks.

richardscarrott commented 7 years ago

That error means the weback-hot-server-middleware is being hit before the compiler is complete which is happening because you're creating and passing two different compilers to weback-dev-middleware and weback-hot-server-middleware.

Your app.js should look more like this:

...
const compiler = webpack([clientConfig, serverConfig]);
// Pass the same `compiler` to both 'webpack-dev-middleware' and 'webpack-hot-server-middleware'
const webpackDevMiddlewareInstance = require('webpack-dev-middleware')(compiler);
const webpackHotServerMiddlewareInstance = require('webpack-hot-server-middleware')(compiler);
app.use(webpackDevMiddlewareInstance);
app.use(webpackHotServerMiddlewareInstance);
...

Additionally, I think you'll run into an error in your server.jsx as it looks like you're expecting the exported function to take an html string which you later pass to cheerio.load... however, it will in fact receive the client stats object (to allow you to reference the webpack assets (with hash) in your html file), the export signature should be this: (stats) => (req, res, next) => void 0.

It's also worth noting that webpack-hot-middleware needs to be mounted in-between webpack-dev-server and webpack-hot-server-middleware because webpack-hot-server-middleware doesn't call next for any requests. You'll also need to filter out the 'server' compiler. I've been meaning to add an example using webpack-hot-middleware to the docs but for reference it should look something like this:

const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client')));
app.use(webpackHotServerMiddleware(compiler));
ghost commented 7 years ago

Thanks so much for the help! I'm still getting the same error and a new one now actually though...

The new error is right after webpack bundles build:

error:  Error: illegal operation on a directory
    at MemoryFileSystem.readFileSync ([...]/node_modules/memory-fs/lib/MemoryFileSystem.js:112:10)
    at MultiCompiler.multiCompiler.plugin.ex ([...]/node_modules/webpack-hot-server-middleware/src/index.js:119:33)

Here's the updated app.js:

...
const webpack = require('webpack');
const clientConfig = require(path.join(__dirname, '/config/webpack.config.client.js'));
const serverConfig = require(path.join(__dirname, '/config/webpack.config.server.js'));
const devServerConfig = clientConfig.devServer;

const compiler = webpack([clientConfig, serverConfig]);

app.use(clientConfig.output.publicPath, express.static(clientConfig.output.path));

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public/favicons')));

if (process.env.NODE_ENV === 'development') {
    const webpackDevMiddlewareInstance = require('webpack-dev-middleware')(compiler);
    const webpackHotServerMiddlewareInstance = require('webpack-hot-server-middleware')(compiler);
    app.use(webpackDevMiddlewareInstance);

    if (devServerConfig.hot) {
        app.use(require('webpack-hot-middleware')(compiler.compilers.find(compiler => compiler.name === 'client'), {
            log: console.log,
            path: '/__webpack_hmr',
            heartbeat: 10000
        }));
    }

    app.use(webpackHotServerMiddlewareInstance);
}

var server = http.createServer(app);

server.listen(port, () => console.log('Server started', server.address()));
...

Updated server.jsx:

...
module.exports = function(stats) {
    console.log(require('util').inspect(stats, { depth: 3 }));

    return function (req, res) {
        var routes = require('./routes.jsx');
        var store = configureStore();

        var css = [];

        match({
            routes: routes.default,
            location: req.originalUrl
        }, (error, redirectLocation, renderProps) => {
            if (error) {
                res.status(500).send(error.message)
            } else if (redirectLocation) {
                res.redirect(302, redirectLocation.pathname + redirectLocation.search)
            } else if (renderProps) {
                var body = ReactDOMServer.renderToString(
                    <ServerRoot store={store} renderProps={renderProps} onInsertCss={(...styles) => {
                        styles.forEach(style => css.push(style._getCss()));
                    }}/>
                );

                const state = store.getState();

                var $ = cheerio.load();

                $('#server-style').html(css.join(''));
                $('#redux-state').html('window.__REDUX_STATE__ = ' + JSON.stringify(state).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--') + ';');
                $('#app').html('<div>' + body + '</div>');

                res.set('content-type', 'text/html');
                res.send($.html());
                res.end();
            } else {
                res.status(404).send('Not found')
            }
        });
    }
}

Let me know if there's anything else I can provide that may be screwing it up. Thanks again.

richardscarrott commented 7 years ago

@DragonFire353 Everything looks alright there in terms of setup so it must be related to to your webpack.config; it might be that you've given your entry point an explicit name other than 'main'?

Are you able to put up a repo so I can reproduce the issue or maybe at least paste your webpack config?

richardscarrott commented 7 years ago

@DragonFire353 I'm going to close this for now as I think it's a usage issue, feel free to continue the discussion here if you like as I may still be able to help.

emmanuel-mendoza commented 7 years ago

Hi,

I am running into a similar issue (related to memory), below you can find my webpack config file and how I use it in the server.

/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack-dev-middleware/node_modules/memory-fs/lib/MemoryFileSystem.js:
112
                        throw new MemoryFileSystemError(errors.code.EISDIR, _path);
         ^
Error
    at MemoryFileSystem.readFileSync (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack-dev-middleware/node_modu
les/memory-fs/lib/MemoryFileSystem.js:112:10)
    at MultiCompiler.multiCompiler.plugin.multiStats (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack-hot-serv
er-middleware/src/index.js:119:33)
    at MultiCompiler.applyPlugins (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/tapable/lib/Tapable.js:26:37)
    at MultiCompiler.<anonymous> (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack/lib/MultiCompiler.js:76:10)
    at Compiler.applyPlugins (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/tapable/lib/Tapable.js:26:37)
    at Watching._done (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack/lib/Compiler.js:78:17)
    at Watching.<anonymous> (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack/lib/Compiler.js:61:18)
    at Compiler.emitRecords (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack/lib/Compiler.js:282:37)
    at Watching.<anonymous> (/Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack/lib/Compiler.js:58:19)
    at /Users/emmanuel/Projects/javascript/react-isomorphic/node_modules/webpack/lib/Compiler.js:275:11

webpack.server.config.js


const webpack = require('webpack');
const path = require('path');
const fs = require('fs');

const SRC_DIR = path.join(__dirname, './src');
const DIST_DIR = path.join(__dirname, './dist');

const clientConfig = {
  name: 'client',
  context: SRC_DIR,
  entry: {
    client: ['react-hot-loader/patch',
      'webpack-hot-middleware/client',
      './client/index'] },
  output: {
    path: DIST_DIR,
    filename: 'bundle.js',
    publicPath: '/static/'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel-loader', 'eslint-loader'],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    root: [
      SRC_DIR
    ]
  },
  target: 'web',
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ],
  node: {
    fs: 'empty' // ,
    // process: false
  },
  bail: false,
  devtool: 'inline-source-map',
  eslint: {
    configFile: './.eslintrc'
  }
};

const serverConfig = {
  name: 'server',
  context: SRC_DIR,
  entry: { server: ['../server.babel'] },
  output: {
    path: DIST_DIR,
    filename: 'server.bundle.js',
    libraryTarget: 'commonjs2'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel-loader', 'eslint-loader'],
        exclude: /node_modules/
      },
      {
        test: /\.json$/,
        loader: 'json-loader'
      }
    ]
  },
  resolve: {
    root: [
      SRC_DIR
    ]
  },
  target: 'node',
  node: {
    process: false
  },
  bail: false,
  externals: fs.readdirSync('node_modules')
    .filter((x) => !x.includes('.bin'))
    .reduce((externals, mod) => {
      const mods = externals;
      mods[mod] = `commonjs ${mod}`;
      return mods;
    }, {}),
  devtool: 'source-map',
  eslint: {
    configFile: './.eslintrc'
  }
};

module.exports = [clientConfig, serverConfig];

server.js

import path from 'path';
import express from 'express';
import bodyParser from 'body-parser';
// HMR
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackHotServerMiddleware from 'webpack-hot-server-middleware';
import config from './webpack.server.config';
// HMR
import routermgr from './src/infra/route-manager';

const app = express();

...

// HMR
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {
  quiet: false,
  publicPath: config[0].output.publicPath
}));
app.use(webpackHotMiddleware(compiler.compilers.find((cmp) => cmp.name === 'client'), {
  log: () => {}
}));
app.use(webpackHotServerMiddleware(compiler));

routermgr.handle(app);

app.listen(3000, () => {
  console.log('Server listening at port 3000');
});

I am pretty sure it is something related to the configuration but I have moved some pieces with no luck.

richardscarrott commented 7 years ago

@emmanuel-mendoza it looks like the entry point to your server bundle is the server itself however webpack-hot-server-middleware expects it to be a middleware function, e.g. https://github.com/60frames/webpack-hot-server-middleware/blob/master/example/server.js

The idea is for your main server to be just a regular, non bundled express server which use()s webpackDevMiddleware, webpackHotMiddleware and webpackHotServerMiddleware, e.g. https://github.com/60frames/webpack-hot-server-middleware/blob/master/example/index.js

emmanuel-mendoza commented 7 years ago

@richardscarrott thank you for pointing that. I needed to refactor the code because of this and node the code is compiling ok (both client and server), but for some reason the server bundle is not recognizing the request object from express lib. I have played a bit with some webpack options but again I feel stuck on this. This is how my code looks like now: webpack configuration file: https://github.com/emmanuel-mendoza/react-isomorphic/blob/master/webpack.server.config.js server middleware file: https://github.com/emmanuel-mendoza/react-isomorphic/blob/master/src/infra/route-manager.js

richardscarrott commented 7 years ago

@emmanuel-mendoza it looks like your middleware (route-manager.js) exports a function expecting the req, res, next args however webpack hot server middleware expects it to export a function of this format -- (stats) => (req, res, next) => void -- so you need to wrap it in another function. e.g. https://github.com/60frames/webpack-hot-server-middleware/blob/master/example/server.js

emmanuel-mendoza commented 7 years ago

@richardscarrott I just tried your suggestion (these have been crazy days at work) and now it works like a charm, thank you!

smashercosmo commented 7 years ago

Also stumbled upon this "serverRenderer is not a function" error. I start the server and this error appears in console until bundle is ready. @richardscarrott how do you think, could that be a reason if my server bundle is much smaller than my client bundle?

elado commented 6 years ago

about illegal operation on a directory error: if you have multi entry server, make sure to specify the chunkName:

entry: {
  index: './src/server.js',
},
app.use(webpackHotServerMiddleware(compiler, { chunkName: 'index' }));

@DragonFire353

barnettx commented 3 years ago

Reiterating what @elado has stated. I was getting TypeError: serverRenderer is not a function error. Turns out it is because the default chunkName used in the middleware is main.

it is assumed that your webpack.config.js uses

entry: './src/server.js'

which will generate the main.bundle.js chunk

if instead you use

entry: {
  server: './src/server.js',
},

then you have to specify your chunkName as an option in the options

app.use(webpackHotServerMiddleware(compiler, { chunkName: 'index' }));