Webpack

Webpack is a powerfull tool. We split the configuration (found in config/webpack) in three different files: default.js, client.js and server.js.

Both client and server configurations extend default, and if more targets (cordova, electron, browser extensions, ...) were to be added to the project, a similar file structure could be kept.

The loaders configuration has been split, for better readability and code reuse.

Default configuration

Default configuration define the common chunks of configuration that will be used whatever the target platform is.

import path from 'path'
import config from '../index'
import loaders from './loaders'

const AUTOPREFIXER_BROWSERS = [
  'Android 2.3',
  'Android >= 4',
  'Chrome >= 35',
  'Firefox >= 31',
  'Explorer >= 9',
  'iOS >= 7',
  'Opera >= 12',
  'Safari >= 7.1'
]

const defaultWebpackConfig = {
  // Input
  context: path.resolve(__dirname, '../../src'),

  // Output
  output: {
    path: path.resolve(__dirname, '../../build'),
    publicPath: '/',
    sourcePrefix: '  '
  },

  // Loaders
  module: { loaders },

  // is it the right place ? at least needed server side for HMR. Is it required if DEBUG == false?
  recordsPath: path.resolve(__dirname, '../../build/_records'),

  resolve: {
    root: path.resolve(__dirname, '../../src'),
    extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx', '.json']
  },

  // Cache generated modules and chunks to improve performance for multiple incremental builds.
  cache: config.DEBUG,

  // Switch loaders to debug mode.
  debug: config.DEBUG,

  // TODO Use debug here to choose value?
  devtool: 'source-map',

  stats: {
    colors: true,
    reasons: config.DEBUG,
    hash: config.VERBOSE,
    version: config.VERBOSE,
    timings: true,
    chunks: config.VERBOSE,
    chunkModules: config.VERBOSE,
    cached: config.VERBOSE,
    cachedAssets: config.VERBOSE
  },

  plugins: [],

  sassLoader: {
    includePaths: [path.resolve(__dirname, '../node_modules')]
  },

  /* eslint-disable global-require */
  postcss (bundler) {
    return [
      require('postcss-import')({ addDependencyTo: bundler }),
      require('precss')(),
      require('autoprefixer')({ browsers: AUTOPREFIXER_BROWSERS })
    ]
  }
  /* eslint-enable global-require */
}

export default defaultWebpackConfig

Client configuration

Client configuration (may be renamed to browser at some point, we’ll see) is in charge of transpiling the browser javascript so it can run in modern browsers.

import path from 'path'
import webpack from 'webpack'
import AssetsPlugin from 'assets-webpack-plugin'
import CompressionPlugin from 'compression-webpack-plugin'
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import config from '../index'
import defaultConfig from './default'

const clientConfig = {
  ...defaultConfig,

  entry: './client.js',

  output: {
    ...defaultConfig.output,
    path: path.join(defaultConfig.output.path, 'public/'),
    filename: config.DEBUG ? '[name].js?[chunkhash]' : '[name].[chunkhash].js',
    chunkFilename: config.DEBUG ? '[name].[id].js?[chunkhash]' : '[name].[id].[chunkhash].js'
  },

  plugins: [
    ...defaultConfig.plugins,

    new webpack.DefinePlugin({ ...config, 'process.env.BROWSER': true }),

    // Emit a file with assets paths
    // https://github.com/sporto/assets-webpack-plugin#options
    new AssetsPlugin({
      path: path.resolve(__dirname, '../../build'),
      filename: 'assets.js',
      processOutput: (x) => `module.exports = ${JSON.stringify(x)};`
    }),

    // Add production plugins if we're doing an optimized build
    ...(!config.DEBUG ? [
      new ExtractTextPlugin('[name].[chunkhash].css', { allChunks: true }),
      new webpack.optimize.DedupePlugin(),
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
          screw_ie8: true,
          // jscs:enable requireCamelCaseOrUpperCaseIdentifiers
          warnings: config.VERBOSE
        }
      }),
      new webpack.optimize.AggressiveMergingPlugin(),
      new CompressionPlugin()
    ] : [])
  ]
}

if (!config.DEBUG) {
  // Production: Replace loaders with "ExtractTextPlugin"
  const originalLoaders = clientConfig.module.loaders[1].loaders
  delete clientConfig.module.loaders[1].loaders
  clientConfig.module.loaders[1].loader = ExtractTextPlugin.extract(...originalLoaders)
}

export default clientConfig

Server configuration

Server configuration is in charge of transpiling code required to run the production-ready express server, to be directly run by the Node interpreter.

import webpack from 'webpack'
import config from '../index'

import defaultConfig from './default'

/**
 * This is Webpack configuration for server side javascript, aimed for a build
 * without intrumentation (like dev middleware or hot module reload).
 *
 * Instrumentation will be added by the runServer script, if needed.
 */
const serverConfig = {
  // Extends common configuration
  ...defaultConfig,

  // Entry point
  entry: './server.js',

  // Output a single server.js, under the build directory
  output: {
    ...defaultConfig.output,
    filename: 'server.js',
    libraryTarget: 'commonjs2'
  },

  // This will be run by node. Also used by our scripts to detect it is
  // the server-side configuration.
  target: 'node',

  // How do we take apart bundlable scripts and external dependencies that we
  // can load from filesystem?
  externals: [
    /^\.\/assets$/,
    function filter (context, request, cb) {
      const isExternal = request.match(/^[@a-z][a-z\/\.\-0-9]*$/i)
      cb(null, Boolean(isExternal))
    }
  ],

  node: {
    console: false,
    global: false,
    process: false,
    Buffer: false,
    __filename: false,
    __dirname: false
  },

  plugins: [
    ...defaultConfig.plugins,
    new webpack.DefinePlugin({ ...config, 'process.env.BROWSER': false }),
    new webpack.BannerPlugin('require("source-map-support").install();',
      { raw: true, entryOnly: false })
  ]
}

export default serverConfig

Loaders configuration

All loaders

import config from '../../index'
import javascriptLoader from './javascript'
import styleLoader from './style'

export default [
  javascriptLoader,
  styleLoader,
  { test: /\.json$/, loader: 'json-loader' },
  { test: /\.txt$/, loader: 'raw-loader' },
  {
    test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    loader: 'url-loader',
    query: {
      name: config.DEBUG ? '[path][name].[ext]?[hash]' : '[hash].[ext]',
      limit: 10000
    }
  },
  {
    test: /\.(ttf|eot|wav|mp3)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    loader: 'file-loader',
    query: {
      name: config.DEBUG ? '[path][name].[ext]?[hash]' : '[hash].[ext]'
    }
  }
]

Javascript

import config from '../../index'
import path from 'path'

export default {
  test: /\.jsx?$/,
  loader: 'babel-loader',
  include: [
    path.resolve(__dirname, '../../../src'),
    path.resolve(__dirname, '../../../config'),
    path.resolve(__dirname, '../../../test'),
    path.resolve(__dirname, '../../../build/assets')
  ],
  query: {
    // https://github.com/babel/babel-loader#options
    cacheDirectory: config.DEBUG,

    // https://babeljs.io/docs/usage/options/
    babelrc: false,
    presets: [
      'react',
      'es2015',
      'stage-0'
    ],
    plugins: [
      'transform-runtime',
      ...config.DEBUG ? [] : [
        'transform-react-remove-prop-types',
        'transform-react-constant-elements',
        'transform-react-inline-elements'
      ]
    ]
  }
}

Style

import config from '../../index'

export default {
  test: /\.scss$/,
  loaders: [
    'style',
    `css?${JSON.stringify({
      // `sourceMap` is set to false because otherwise, there will be a problem with custom fonts
      // when using the development proxy.
      // See http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts
      sourceMap: false,
      // CSS Modules https://github.com/css-modules/css-modules
      // modules: true,
      localIdentName: config.DEBUG ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]',
      // CSS Nano http://cssnano.co/options/
      minimize: !config.DEBUG
    })}!sass?${JSON.stringify({
      sourceMap: config.DEBUG
    })}`
  ]
}