shellscape/koa-webpack

Hot reloading doesn't work in 2.0

mrchief opened this issue ยท 11 comments

  • Node Version: 8.9.4
  • NPM Version: 5.2.0
  • koa Version: ^2.3.0
  • koa-webpack: ^2.0.3
v2 webpack.config
// webpack.config.js
'use strict' // eslint-disable-line

const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const StyleLintPlugin = require('stylelint-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { CSSModules, inlineLimit } = require('./config')

const nodeEnv = process.env.NODE_ENV || 'development'
const isDev = nodeEnv !== 'production'

const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin')
const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./WIT.config')).development(isDev)

// Setting the plugins for development/production
const getPlugins = () => {
  // Common
  const plugins = [
    new ExtractTextPlugin({
      filename: '[name].[contenthash:8].css',
      allChunks: true,
      disable: isDev, // Disable css extracting on development
      ignoreOrder: CSSModules
    }),
    new webpack.LoaderOptionsPlugin({
      options: {
        // Javascript lint
        eslint: {},
        context: '/', // Required for the sourceMap of css/sass loader
        debug: isDev,
        minimize: !isDev
      }
    }),
    // Style lint
    new StyleLintPlugin({ files: ['src/**/*.css'], quiet: false }),
    // Setup environment variables for client
    new webpack.EnvironmentPlugin({ NODE_ENV: JSON.stringify(nodeEnv) }),
    // Setup global variables for client
    new webpack.DefinePlugin({
      __CLIENT__: true,
      __SERVER__: false,
      __DEV__: isDev
    }),
    new webpack.NoEmitOnErrorsPlugin(),
    webpackIsomorphicToolsPlugin,
    new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: module => /node_modules/.test(module.resource) })
  ]

  if (isDev) {
    // For development
    plugins.push(
      // Prints more readable module names in the browser console on HMR updates
      new webpack.NamedModulesPlugin(),
      new webpack.IgnorePlugin(/webpack-stats\.json$/),
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: false
      })
    )
  } else {
    plugins.push(
      // For production
      new webpack.HashedModuleIdsPlugin(),
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        beautify: false,
        mangle: { screw_ie8: true },
        compress: {
          screw_ie8: true, // React doesn't support IE8
          warnings: false,
          unused: true,
          dead_code: true
        },
        output: { screw_ie8: true, comments: false }
      })
    )
  }

  return plugins
}

// get babel plugins for dev/production
const getBabelPlugins = () => {
  const plugins = [
    'react-hot-loader/babel',
    'transform-object-rest-spread',
    'transform-class-properties',
    [
      'transform-imports',
      {
        'redux-form': {
          transform: 'redux-form/es/${member}', // eslint-disable-line no-template-curly-in-string
          preventFullImport: true
        },
        lodash: {
          transform: 'lodash/${member}', // eslint-disable-line no-template-curly-in-string
          preventFullImport: true
        }
      }
    ]
  ]

  if (isDev) {
    plugins.push('transform-react-jsx-source')
  }

  return plugins
}

// Setting the entry for development/production
const getEntry = () => {
  // For development
  let entry = [
    'babel-polyfill', // Support promise for IE browser (for dev)
    'react-hot-loader/patch',
    './src/client.js'
  ]

  // For production
  if (!isDev) {
    entry = {
      main: './src/client.js'
    }
  }

  return entry
}

// Setting webpack config
module.exports = {
  name: 'client',
  target: 'web',
  cache: isDev,
  devtool: isDev ? 'cheap-module-eval-source-map' : 'hidden-source-map',
  context: path.join(process.cwd()),
  entry: getEntry(),
  output: {
    path: path.join(process.cwd(), './build/public/assets'),
    publicPath: '/assets/',
    // Don't use chunkhash in development it will increase compilation time
    filename: isDev ? '[name].js' : '[name].[chunkhash:8].js',
    chunkFilename: isDev ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
    pathinfo: isDev
  },
  module: {
    noParse: [/dtrace-provider/, /safe-json-stringify/, /mv/, /source-map-support/],
    rules: [
      {
        test: /\.jsx?$/,
        enforce: 'pre',
        exclude: /node_modules/,
        loader: 'eslint',
        options: {
          fix: true
        }
      },
      {
        test: /node_modules[/\\]jsonstream/i,
        loader: 'shebang'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel',
        exclude: /node_modules/,
        options: {
          cacheDirectory: isDev,
          babelrc: false,
          presets: [
            [
              'env',
              {
                targets: {
                  browsers: ['last 2 versions', 'ie >= 10']
                },
                useBuiltIns: true
              }
            ],
            'react'
          ],
          plugins: getBabelPlugins()
        }
      },
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ExtractTextPlugin.extract({
          fallback: 'style',
          use: [
            {
              loader: 'css',
              options: {
                importLoaders: 1,
                sourceMap: true,
                modules: CSSModules,
                // "context" and "localIdentName" need to be the same with server config,
                // or the style will flick when page first loaded
                context: path.join(process.cwd(), './src'),
                localIdentName: isDev ? '[name]__[local].[hash:base64:5]' : '[hash:base64:5]',
                minimize: !isDev
              }
            },
            {
              loader: 'postcss'
            }
          ]
        })
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader'
            }
          ]
        }),
        include: [/node_modules[/\\]animate.css/,
          /flexboxgrid/,
          /react-table/,
          /react-datepicker/,
          /react-checkbox/
        ]
      },
      {
        test: /\.(woff2?|ttf|eot|svg)$/,
        loader: 'url',
        options: { limit: inlineLimit }
      },
      {
        test: webpackIsomorphicToolsPlugin.regular_expression('images'),
        // Any image below or equal to 10K will be converted to inline base64 instead
        use: [
          {
            loader: 'url',
            options: { limit: inlineLimit }
          },
          // Using for image optimization
          {
            loader: 'image-webpack',
            options: { bypassOnDebug: true }
          }
        ]
      }
    ]
  },
  plugins: getPlugins(),
  // Where to resolve our loaders
  resolveLoader: {
    modules: ['src', 'node_modules'],
    moduleExtensions: ['-loader']
  },
  resolve: {
    modules: ['src', 'node_modules'],
    descriptionFiles: ['package.json'],
    moduleExtensions: ['-loader'],
    extensions: ['.js', '.jsx', '.json']
  },
  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  // https://webpack.github.io/docs/configuration.html#node
  // https://github.com/webpack/node-libs-browser/tree/master/mock
  node: {
    fs: 'empty',
    vm: 'empty',
    net: 'empty',
    tls: 'empty'
  }
}
v2 app.js
// app.js

  const config = require('../tools/webpack/webpack.client.babel')

  app.use(require('koa-webpack')({
    config,
    dev: {
      publicPath: config.output.publicPath,
      noInfo: true,
      stats: 'minimal'
    },
    hot: { port: 3001 }  // this bit was added after 2.x migration as the default port 8081 is taken up in my PC
  }))

Expected Behavior

Hot reloading should work. Browser should show HMR connected in console.

Actual Behavior

Doesn't work. I neither see HMR connected message nor does the browser refreshes when I change source files. Webpack builds the changes.

How can we reproduce the behavior?

The same config (minus changes required for v2.x migration) worked without any issues with v1. Here are the relevant bits:

v1 webpack config
'use strict' // eslint-disable-line

const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const StyleLintPlugin = require('stylelint-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { CSSModules, inlineLimit } = require('./config')

const nodeEnv = process.env.NODE_ENV || 'development'
const isDev = nodeEnv !== 'production'

const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin')
const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./WIT.config')).development(isDev)

// Setting the plugins for development/production
const getPlugins = () => {
  // Common
  const plugins = [
    new ExtractTextPlugin({
      filename: '[name].[contenthash:8].css',
      allChunks: true,
      disable: isDev, // Disable css extracting on development
      ignoreOrder: CSSModules
    }),
    new webpack.LoaderOptionsPlugin({
      options: {
        // Javascript lint
        eslint: {},
        context: '/', // Required for the sourceMap of css/sass loader
        debug: isDev,
        minimize: !isDev
      }
    }),
    // Style lint
    new StyleLintPlugin({ files: ['src/**/*.css'], quiet: false }),
    // Setup environment variables for client
    new webpack.EnvironmentPlugin({ NODE_ENV: JSON.stringify(nodeEnv) }),
    // Setup global variables for client
    new webpack.DefinePlugin({
      __CLIENT__: true,
      __SERVER__: false,
      __DEV__: isDev
    }),
    new webpack.NoEmitOnErrorsPlugin(),
    webpackIsomorphicToolsPlugin,
    new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: module => /node_modules/.test(module.resource) })
  ]

  if (isDev) {
    // For development
    plugins.push(
      new webpack.HotModuleReplacementPlugin(),
      // Prints more readable module names in the browser console on HMR updates
      new webpack.NamedModulesPlugin(),
      new webpack.IgnorePlugin(/webpack-stats\.json$/),
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: false
      })
    )
  } else {
    plugins.push(
      // For production
      new webpack.HashedModuleIdsPlugin(),
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        beautify: false,
        mangle: { screw_ie8: true },
        compress: {
          screw_ie8: true, // React doesn't support IE8
          warnings: false,
          unused: true,
          dead_code: true
        },
        output: { screw_ie8: true, comments: false }
      })
    )
  }

  return plugins
}

// get babel plugins for dev/production
const getBabelPlugins = () => {
  const plugins = [
    'react-hot-loader/babel',
    'transform-object-rest-spread',
    'transform-class-properties',
    [
      'transform-imports',
      {
        'redux-form': {
          transform: 'redux-form/es/${member}', // eslint-disable-line no-template-curly-in-string
          preventFullImport: true
        },
        lodash: {
          transform: 'lodash/${member}', // eslint-disable-line no-template-curly-in-string
          preventFullImport: true
        }
      }
    ]
  ]

  if (isDev) {
    plugins.push('transform-react-jsx-source')
  }

  return plugins
}

// Setting the entry for development/production
const getEntry = () => {
  // For development
  let entry = [
    'babel-polyfill', // Support promise for IE browser (for dev)
    'react-hot-loader/patch',
    'webpack-hot-middleware/client?reload=true',
    './src/client.js'
  ]

  // For production
  if (!isDev) {
    entry = {
      main: './src/client.js'
    }
  }

  return entry
}

// Setting webpack config
module.exports = {
  name: 'client',
  target: 'web',
  cache: isDev,
  devtool: isDev ? 'cheap-module-eval-source-map' : 'hidden-source-map',
  context: path.join(process.cwd()),
  entry: getEntry(),
  output: {
    path: path.join(process.cwd(), './build/public/assets'),
    publicPath: '/assets/',
    // Don't use chunkhash in development it will increase compilation time
    filename: isDev ? '[name].js' : '[name].[chunkhash:8].js',
    chunkFilename: isDev ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
    pathinfo: isDev
  },
  module: {
    noParse: [/dtrace-provider/, /safe-json-stringify/, /mv/, /source-map-support/],
    rules: [
      {
        test: /\.jsx?$/,
        enforce: 'pre',
        exclude: /node_modules/,
        loader: 'eslint',
        options: {
          fix: true
        }
      },
      {
        test: /node_modules[/\\]jsonstream/i,
        loader: 'shebang'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel',
        exclude: /node_modules/,
        options: {
          cacheDirectory: isDev,
          babelrc: false,
          presets: [
            [
              'env',
              {
                targets: {
                  browsers: ['last 2 versions', 'ie >= 10']
                },
                useBuiltIns: true
              }
            ],
            'react'
          ],
          plugins: getBabelPlugins()
        }
      },
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ExtractTextPlugin.extract({
          fallback: 'style',
          use: [
            {
              loader: 'css',
              options: {
                importLoaders: 1,
                sourceMap: true,
                modules: CSSModules,
                // "context" and "localIdentName" need to be the same with server config,
                // or the style will flick when page first loaded
                context: path.join(process.cwd(), './src'),
                localIdentName: isDev ? '[name]__[local].[hash:base64:5]' : '[hash:base64:5]',
                minimize: !isDev
              }
            },
            {
              loader: 'postcss'
            }
          ]
        })
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader'
            }
          ]
        }),
        include: [/node_modules[/\\]animate.css/,
          /flexboxgrid/,
          /react-table/,
          /react-datepicker/,
          /react-checkbox/
        ]
      },
      {
        test: /\.(woff2?|ttf|eot|svg)$/,
        loader: 'url',
        options: { limit: inlineLimit }
      },
      {
        test: webpackIsomorphicToolsPlugin.regular_expression('images'),
        // Any image below or equal to 10K will be converted to inline base64 instead
        use: [
          {
            loader: 'url',
            options: { limit: inlineLimit }
          },
          // Using for image optimization
          {
            loader: 'image-webpack',
            options: { bypassOnDebug: true }
          }
        ]
      }
    ]
  },
  plugins: getPlugins(),
  // Where to resolve our loaders
  resolveLoader: {
    modules: ['src', 'node_modules'],
    moduleExtensions: ['-loader']
  },
  resolve: {
    modules: ['src', 'node_modules'],
    descriptionFiles: ['package.json'],
    moduleExtensions: ['-loader'],
    extensions: ['.js', '.jsx', '.json']
  },
  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  // https://webpack.github.io/docs/configuration.html#node
  // https://github.com/webpack/node-libs-browser/tree/master/mock
  node: {
    fs: 'empty',
    vm: 'empty',
    net: 'empty',
    tls: 'empty'
  }
}
v1 app.js
// app.js

  const config = require('../tools/webpack/webpack.client.babel')

  app.use(require('koa-webpack')({
    config,
    dev: {
      publicPath: config.output.publicPath,
      noInfo: true,
      stats: 'minimal'
    }
  }))

Dunno what to tell you. Works great on my end. Must be an environment or config issue. Might want to start from a simpler config and build it back up to identify the issue.

You mean you have a koa app using react and hot reloading works for you? Mind sharing some code?

There is literally 3 changed lines between v1 and v2 config/app.js. So I'm puzzled as to what I'm missing.

I don't use or work with React. So you or someone else who uses that is going to have to debug the reasons why it's not working as expected. I can tell you however, that the create-react-app folks are using this module via a not-as-yet-public project with success.

I can tell you however, that the create-react-app folks are using this module via a not-as-yet-public project with success.

I heard those rumors too. :)

I tried searching but didn't find any boilerplate that uses webpack-hot-client. Almost everyone seems to be using webpack-hot-middleware and that works like a charm in my case too. I'll keep looking though. Let me know if you stumble upon anything.

Meanwhile, will you be able to share a gist of your working config? Just the config and app.js will suffice. Maybe I'd be able to glean something off it?

Same problem here. Websocket is up but the client is not listening. Although I'm not using React, and I'm not sure that matters(?).

There is no WebSocket Client Connected in the log. But if I connect manually the log message pop up. So I think that the javascript that is suppose to connect to the websocket is never given to the client for some reason

There are more flags too: if you pass the koa server to webpack-hot-client, it gives you some weird error.

Almost everyone seems to be using webpack-hot-middleware and that works like a charm in my case too.

You're not required to use this module if you'd prefer webpack-hot-middleware, and I don't mean that in a snarky way. You can also use the older version. webpack-hot-middleware is a much older approach that relies on some older features of browsers before WebSockets were available. webpack-hot-client also utilizes webpack internals to take some work off the shoulders of devs. Mentioned at the bottom of the readme is also koa-webpack-middleware, which is the super old project an earlier version of this module was forked from. If the current line of koa-webpack isn't your bag or your preference, you have a bunch of options.

Meanwhile, will you be able to share a gist of your working config? Just the config and app.js will suffice. Maybe I'd be able to glean something off it?

Both the tests for koa-webpack and webpack-hot-client contain configs that work perfectly fine. Again, I recommend you start with a simple config and build it back up to the current, rather complex state you have it in now. Going step by step in adding complexity and components onto a config should yield the issue or the culprit, and it should be a relatively quick process.

You can also examine your build output in the terminal to verify that the webpack-hot-client/client script was added to your bundle. If it wasn't, nothing will happen in the client.

There are more flags too: if you pass the koa server to webpack-hot-client, it gives you some weird error.

I dunno what that means, but if an option doesn't work correctly you should open a new issue rather than reporting it in this thread.

@MichaelBergquistSuarez since you're adding a comment here with a difference scenario than the OP, you should open a new issue and provide the code the issue template requests. That comment doesn't add much value or help to triage the original post.

You're not required to use this module if you'd prefer webpack-hot-middleware, and I don't mean that in a snarky way.

I came to using this module since it saved me effort from using those two.

You can also use the older version.

Its what I'm doing right now. But I'd like to be able to use the new version. I like this module and reported this only because I care about it.

Again, I recommend you start with a simple config and build it back up to the current, rather complex state you have it in now.

I only pasted everything for completeness. The relevant bits are few lines. I'll try to create a repo with simplified config.

I just published a patch version to webpack-hot-client that might resolve one or more issues you all are facing. Please do let me know if that works. Will probably require a reinstall of koa-webpack.

Success! Good work. Now the client connects to the socket, at least on my part (not using React though).

Using the master branch from both koa-webpack and webpack-hot-client

Works like a charm!

image

And love the new icon in the console log! ๐Ÿ‘