Home Reference Source Repository

src/devtools.js

import Webpack          from 'webpack'
import Path             from 'path'
import AssetsPlugin     from 'webpack-assets-plugin'
import CleanPlugin      from 'clean-webpack-plugin'
import nodeExternals    from 'webpack-node-externals'
import devMiddleware    from 'webpack-dev-middleware'
import hotMiddleware    from 'webpack-hot-middleware'
import extendify        from 'custom-extend'

const merge = extendify({
  inPlace: false,
  isDeep: true,
  arrays: 'concat'
})

const __PRODUCTION__      = (process.env.NODE_ENV === 'production')
const __DEVELOPMENT__     = !(__PRODUCTION__)

export default class DevTools {

  static baseConfig = {
    environment: [],
    define: {},

    output: {
      filename: '[name].js',
      path: 'public',
      publicPath: '/assets/',
    },

    commonsChunk: {},
    vendorLibs: [
      'flux-standard-action',
      'isomorphic-fetch',
      'lodash',
      'protium',
      'qs',
      'react',
      'react-cookie',
      'react-dom',
      'react-helmet',
      'react-hot-loader/patch',
      'react-redux',
      'react-router',
      'react-router-bootstrap',
      'react-router-redux',
      'react-router-scroll/lib/useScroll',
      'redux',
      'redux-actions',
      'redux-connect',
      'redux-promise',
    ],

    module: {
      preLoaders: [
        {
          test: /\.jsx?$/,
          loader: "source-map-loader"
        }
      ],
      loaders: [
        {
          test: /\.jsx?$/,
          loaders: ['babel'],
          exclude: /node_modules/
        }
      ]
    },

    devtool: 'source-map'
  }

  constructor(cwd) {
    this.cwd = Path.resolve(cwd)
  }

  serverConfig(entrypoint, options = {}) {

    const opts = merge({}, DevTools.baseConfig, options)

    const entry = Array.isArray(entrypoint) ? entrypoint : [entrypoint]

    const config = {}

    config.context = this.cwd
    config.devtool = opts.devtool

    config.entry = { server: entry }

    config.output = opts.output
    config.output.path = Path.resolve(config.context, 'public')
    config.output.libraryTarget = 'commonjs'

    config.module = opts.module
    config.externals = [nodeExternals()]

    config.target = 'node'
    config.node = {
      console: false,
      global: false,
      process: false,
      Buffer: false,
      __filename: false,
      __dirname: false,
      setImmediate: false
    }

    delete config.hot
    delete config.define

    return config
  }

  browserConfig(entrypoint, options = {}) {

    const opts = merge({}, DevTools.baseConfig, options)

    const entry = Array.isArray(entrypoint) ? entrypoint : [entrypoint]

    const config = {}

    config.context = this.cwd
    config.devtool = opts.devtool

    config.entry = { 
      client: entry,
      vendor: opts.vendorLibs
    }

    config.output = opts.output
    config.output.filename = '[name].js'
    config.output.library = '__APPLICATION__'
    config.output.libraryTarget = 'var'

    if (config.output.path === DevTools.baseConfig.output.path) {
      config.output.path = Path.resolve(config.context, 'public')
    }

    config.module = opts.module

    config.plugins = [
      new Webpack.optimize.CommonsChunkPlugin({
        name: 'vendor', 
        filename: 'vendor.js',
        async: false
      }),
      new AssetsPlugin({ 
        assetsRegex: /\.(jpe?g|png|gif|svg|scss|sass|css)$/i,
        metadata: true,
        prettyPrint: true,
        filename: 'assets.json'
      }),
      new Webpack.EnvironmentPlugin([
        'NODE_ENV',
        ...opts.environment
      ]),
      new Webpack.DefinePlugin({
        __CLIENT__: true,
        __SERVER__: false,
        __DEVELOPMENT__: __DEVELOPMENT__,
        __PRODUCTION__: __PRODUCTION__,
        ...opts.define
      }),
      ...opts.plugins || []
    ]

    if (__DEVELOPMENT__ && opts.hot) {
      config.entry.client.unshift(
        'webpack-hot-middleware/client',
        'webpack/hot/only-dev-server',
        'react-hot-loader/patch'
      )

      config.plugins.push(
        new Webpack.HotModuleReplacementPlugin(),
        new Webpack.IgnorePlugin(/webpack-stats\.json$/)
      )
    }

    return config
  }

  devMiddleware(config, options = {}) {
    let browserConfig, serverConfig

    if (Array.isArray(config)) {
      browserConfig = config[0]
      serverConfig = config[1]
    } else {
      browserConfig = config.browser
      serverConfig = config.server
    }

    // SERVER SIDE //
    const serverCompiler = Webpack(serverConfig)
    serverCompiler.watch({}, (err, stats)=> {
      if (err) {
        throw err
      }
      if (stats.hasWarnings() || stats.hasErrors()) {
        console.log(stats.toString(statsOptions))
      } else {
        let s = stats.toJson()
        console.log(`(server) webpack built ${s.hash} in ${s.time}ms`)
      }
    })


    // CLIENT SIDE //
    const devCompiler = Webpack(browserConfig)
    const devDefaultOptions = {
      quiet: true,
      noInfo: true,
      hot: true,
      inline: true,
      lazy: false,
      publicPath: browserConfig.output.publicPath,
      headers: {'Access-Control-Allow-Origin': '*'},
      stats: {colors: true}
    }

    const devOptions = {...devDefaultOptions, ...(options.middleware || {})}

    const statsDefaults = {
      hash: false,
      version: false,
      timings: true,
      assets: true,
      chunks: false,
      chunkModules: false,
      modules: false,
      colors: true
    }

    const statsOptions = {...statsDefaults, ...(options.stats || {})}

    return [
      devMiddleware(devCompiler, devOptions),
      hotMiddleware(devCompiler)
    ]
  }

}