webpack.configはCoffeeScriptで書くと幸せになれる

「CoffeeScript信者として何か書かねばならぬが、ネタが思い浮かばないぜ。」

皆さん日々、四苦八苦しながらwebpack.configを書いているでしょうか?その四苦八苦、CoffeeScriptを使うと少し楽になりますよ。

# webpack.configをCoffeeScriptで使う方法

まずは、coffeescriptをpackageに含めます。

```shell-session
$ yarn add -D coffeescript
```

あとは拡張子を`.coffee`にして`webpack.config.coffee`とするだけです。偉いぞwebpack!

# CoffeeScriptの何が嬉しいか

## 記述が簡潔になる

CoffeeScriptを使うと記述が簡潔になる気がします。実際に見比べてみてください。

```js:webpack.config.js
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

let loader = {};
loader.coffee = ['babel-loader', 'coffee-loader'];
loader.css = [
  { loader: 'css-loader'    , options: { sourceMap: true } },
  { loader: 'postcss-loader', options: { sourceMap: true } },
  { loader: 'stylus-loader' , options: { sourceMap: true } },
];
loader.stylus = [
  { loader: 'style-loader'  , options: { sourceMap: true } },
  { loader: 'css-loader'    , options: { sourceMap: true } },
  { loader: 'postcss-loader', options: { sourceMap: true } },
  { loader: 'stylus-loader' , options: { sourceMap: true } },
  {
    loader: 'stylus-resources-loader',
    options: {
      resources: [
        path.resolve(__dirname, './src/styles/_variables.styl'),
        path.resolve(__dirname, './src/styles/_mixins.styl')
      ]
    }
  },
];
loader.json = ['json-loader'];

baseConfig = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: ''
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
          options: {
            loaders: {
              coffee: loader.coffee,
              stylus: loader.stylus
            }
          }
        }
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]?[hash]'
            }
          }
        ]
      },
      {
        test: /\.coffee$/,
        use: loader.coffee
      },
      {
        test: /\.css$/,
        use: loader.css
      },
      {
        test: /\.styl$/,
        use: loader.stylus
      },
      {
        test: /\.json$/,
        use: loader.json
      }
    ]
  },
  resolve: {
    alias: {
      '@root':       path.resolve(__dirname),
      '@src':        path.resolve(__dirname, 'src'),
      '@scripts':    path.resolve(__dirname, 'src', 'scripts'),
      '@components': path.resolve(__dirname, 'src', 'components'),
      '@pages':      path.resolve(__dirname, 'src', 'pages'),
      '@assets':     path.resolve(__dirname, 'src', 'assets'),
      '@styles':     path.resolve(__dirname, 'src', 'styles'),
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src', 'index_template.html')
    }),
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: `'${process.env.NODE_ENV}'`
      }
    }),
    new webpack.ProvidePlugin({
      '_': 'lodash',
      'axios': 'axios'
    })
  ]
};

if (process.env.NODE_ENV === 'production') {
  config = merge(baseConfig, {
    output: {
      filename: 'build-[hash].js'
    },
    devtool: '#source-map',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        compress: {
          warnings: false
        }
      }),
      new webpack.LoaderOptionsPlugin({
        minimize: true
      })
    ]
  });
} else {
  config = merge(baseConfig, {
    output: {
      filename: 'build.js'
    },
    devtool: '#eval-source-map',
    devServer: {
      contentBase: 'dist',
      historyApiFallback: true,
      noInfo: true
    },
    performance: {
      hints: false
    }
  });
}

module.exports = config;
```

```coffeescript:webpack.config.coffee
path = require('path')
webpack = require('webpack')
merge = require('webpack-merge')
HtmlWebpackPlugin = require('html-webpack-plugin')
CleanWebpackPlugin = require('clean-webpack-plugin')

loader = {}
loader.coffee = ['babel-loader', 'coffee-loader']
loader.css    = [
  { loader: 'css-loader'    , options: sourceMap: true }
  { loader: 'postcss-loader', options: sourceMap: true }
  { loader: 'stylus-loader' , options: sourceMap: true }
]
loader.stylus = [
  { loader: 'style-loader'  , options: sourceMap: true }
  { loader: 'css-loader'    , options: sourceMap: true }
  { loader: 'postcss-loader', options: sourceMap: true }
  { loader: 'stylus-loader' , options: sourceMap: true }
  {
    loader: 'stylus-resources-loader'
    options:
      resources: [
        path.resolve(__dirname, './src/styles/_variables.styl')
        path.resolve(__dirname, './src/styles/_mixins.styl')
      ]
  }
]
loader.json = ['json-loader']

baseConfig =
  entry: './src/main.js'
  output:
    path: path.resolve(__dirname, './dist')
    publicPath: ''
  module:
    rules: [
      test: /\.vue$/
      use:
        loader: 'vue-loader'
        options:
          loaders:
            coffee: loader.coffee
            stylus: loader.stylus
    ,
      test: /\.(png|jpg|gif|svg)$/
      use: [
        loader: 'file-loader'
        options:
          name: '[name].[ext]?[hash]'
      ]
    ,
      test: /\.coffee$/
      use: loader.coffee
    ,
      test: /\.css$/
      use: loader.css
    ,
      test: /\.styl$/
      use: loader.stylus
    ,
      test: /\.json$/
      use: loader.json
    ]
  resolve:
    alias:
      '@root':       path.resolve(__dirname)
      '@src':        path.resolve(__dirname, 'src')
      '@scripts':    path.resolve(__dirname, 'src', 'scripts')
      '@components': path.resolve(__dirname, 'src', 'components')
      '@pages':      path.resolve(__dirname, 'src', 'pages')
      '@assets':     path.resolve(__dirname, 'src', 'assets')
      '@styles':     path.resolve(__dirname, 'src', 'styles')
      'vue$': 'vue/dist/vue.esm.js'
  plugins: [
    new HtmlWebpackPlugin
      template: path.resolve(__dirname, 'src', 'index_template.html')
    new webpack.DefinePlugin
      'process.env':
        NODE_ENV: "'#{process.env.NODE_ENV}'"
    new webpack.ProvidePlugin
      '_': 'lodash'
      'axios': 'axios'
  ]

if process.env.NODE_ENV == 'production'
  config = merge baseConfig,
    output:
      filename: 'build-[hash].js'
    devtool: '#source-map'
    plugins: [
      new CleanWebpackPlugin(['dist'])
      new webpack.optimize.UglifyJsPlugin
        sourceMap: true
        compress:
          warnings: false
      new webpack.LoaderOptionsPlugin
        minimize: true
    ]
else
  config = merge baseConfig,
    output:
      filename: 'build.js'
    devtool: '#eval-source-map'
    devServer:
      contentBase: 'dist'
      historyApiFallback: true
      noInfo: true
    performance:
      hints: false

module.exports = config
```

どうでしょうか?`webpack.config.coffee`のほうが余計なブレースやカンマがなくなり、スッキリした見た目になっていると思いませんか?そうでもないですか?

まぁ、分かる人にだけわかってもらえれば結構。

## 試行錯誤しやすい

webpack.configを試行錯誤しながら編集することはままあります。そんなときjsだと結構いらいらさせられます。

```diff:webpack.config.js
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        compress: {
          warnings: false
        }
      }),
      new webpack.LoaderOptionsPlugin({
        minimize: true
      })
+     new CleanWebpackPlugin(['dist']) // pluginを足してみて動きを確かめたい。
    ]
```

この`weboack.config.js`はシンタックスエラーになります。追加した行の手前にカンマがないからです。憎い！

CoffeeScriptならこのようなイライラとは無縁です。

```diff:webpack.config.coffee
    plugins: [
      new webpack.optimize.UglifyJsPlugin
        sourceMap: true
        compress:
          warnings: false
-     new webpack.LoaderOptionsPlugin # 削除も
-       minimize: true
+     new CleanWebpackPlugin(['dist']) # 追加も自由自在！
    ]
```

# まとめ

どうですか?CoffeeScriptが使ってみたくなりましたか?なりませんか?そうですか。残念です・・・。