webpack -> FuseBox 移行メモ

`webpack`を使っていたプロジェクトを`FuseBox`に移行してみました。

バンドル時間が超速くなり、バンドルファイルサイズが超小さくなりました。

webpack
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/c90d3e69-9009-c24f-d719-435bce19ef87.png)

FuseBox
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/46592cdb-a9af-ecc3-43fd-62934050c61a.png)


## package.json

`package.json`の比較です。

```json:package.json(webpack)
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.23.1",
    "babel-eslint": "^8.0.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-flow": "^6.23.0",
    "babel-preset-react": "^6.23.0",
    "css-loader": "^0.28.7",
    "eslint": "^4.6.1",
    "eslint-loader": "^1.6.3",
    "eslint-plugin-flowtype": "^2.34.1",
    "eslint-plugin-react": "^7.3.0",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^0.11.2",
    "flow-bin": "^0.54.1",
    "node-sass": "^4.5.0",
    "sass-loader": "^6.0.3",
    "style-loader": "^0.18.2",
    "url-loader": "^0.5.8",
    "webpack": "^3.4.1"
  },
```

```json:package.json(FuseBox)
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.23.1",
    "babel-eslint": "^8.0.0",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-flow": "^6.23.0",
    "babel-preset-react": "^6.23.0",
    "eslint": "^4.6.1",
    "eslint-plugin-flowtype": "^2.34.1",
    "eslint-plugin-react": "^7.3.0",
    "flow-bin": "^0.54.1",
    "fuse-box": "^2.2.31",
    "node-sass": "^4.5.0",
    "uglify-js": "^3.1.1"
  },
```

FuseBoxは自パッケージ内のプラグインで大体のバンドルを済ませることができます。
webpackのようになんとかloader系をいちいちインストールしなくてもよいので、`package.json`はかなりスッキリしました。


## 設定ファイル

もともとのwebpackの設定`webpack.config.dev.js`と、FuseBoxの設定`fuse.js`です。
だいたい同じことを実現できていると思いますが、ESLint関連だけはnpm scriptsからの実行に切り替えました。

```js:webpack.config.dev.js
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const path = require('path');

module.exports = {
  devtool: 'inline-source-map',
  entry: {
    bundle: './src/javascripts/index.js',
  },
  output: {
    path: path.join(__dirname, '../../public/javascripts'),
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: ['eslint-loader'],
        enforce: 'pre'
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['react', 'es2015', 'flow'],
        }
      },
      {
        test: /\.(css|scss)$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      },
      {
        test: /\.(jpg|png)$/,
        use: ['url-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  plugins: [
    new ExtractTextPlugin('[name].css'),
    new webpack.LoaderOptionsPlugin({
      options: {
        eslint: {
          test: /\.jsx?$/,
          configFile: path.join(__dirname, '../../.eslintrc'),
          cache: false
        }
      }
    })
  ],
};
```

```js:fuse.js
const {
  FuseBox,
  CSSPlugin,
  SassPlugin,
  BabelPlugin,
  QuantumPlugin,
  WebIndexPlugin,
  ImageBase64Plugin,
  JSONPlugin,
  Sparky
} = require('fuse-box')

let fuse, app, vendor, isProduction

Sparky.task('config', () => {
  fuse = new FuseBox({
    homeDir: 'src/',
    sourceMaps: !isProduction,
    hash: isProduction,
    output: 'public/$name.js',
    target: 'browser',
    experimentalFeatures: true,
    plugins: [
      BabelPlugin({
        sourceMaps: !isProduction,
        presets: ['es2015', 'react', 'flow'],
      }),
      CSSPlugin(),
      [SassPlugin(), CSSPlugin()],
      ImageBase64Plugin(),
      JSONPlugin(),
      WebIndexPlugin({
        template: 'src/index.html'
      }),
      isProduction && QuantumPlugin({
        bakeApiIntoBundle: 'bundle',
        treeshake: true,
        uglify: true,
      }),
    ],
  })

  app = fuse.bundle('bundle').instructions('> javascripts/index.js')
})

Sparky.task('clean', () => Sparky.src('public/').clean('public/'))
Sparky.task('copy', () => Sparky.src('index.html', {base: 'public/'}).dest('./'))
Sparky.task('prod-env', () => { isProduction = true })
Sparky.task('build', ['prod-env', 'clean', 'config'], () => fuse.run())
Sparky.task('release', ['build', 'copy'], () => {})

Sparky.task('default', ['clean', 'config'], () => {
  fuse.dev({
    port: 8000,
  })
  app.watch().hmr()
  return fuse.run()
})
```

---

`fuse.js`は、[fuse-box/react-example](https://github.com/fuse-box/react-example)を参考にして作りました。

FuseBox付属のタスクランナー[Sparky](http://fuse-box.org/page/sparky)を使用しています。
詳しい使い方は[参考サイト](https://tech.recruit-mp.co.jp/front-end/post-12727/)に譲ります。
リリース用にビルドするときのみ`prod-env`タスクを噛ませて`isProduction`フラグを立てることにより、`sourceMaps`を消したりuglifyしたりできて、かなり気持ちいいです。

## 知見

`fuse.js`を書くときにいろいろと迷ったので、個人的なポイントやハマったところを挙げていきます。

### デフォルトのトランスパイラがTypeScript

README.mdに

> FuseBox loves typescript, and does not require any additional configuration.

とある通り、特に何も指定しなければソースコードをTypeScriptと解釈してトランスパイルします。
既存のプロジェクトでBabelを使用している場合、`plugins`に`BabelPlugin`を指定する必要があります。
`.babelrc`に記載していたようなpresetsの設定等はこの中に書きます。


### `QuantumPlugin`

バンドルしたソースコードをminifyしたりtreeshakeしたりuglifyしたりできます。
このプラグインを追加すると、バンドルファイルと別に`api.js`というファイルが吐かれるようになります。
それも含めて一つのバンドルファイルにしたい場合、`bakeApiIntoBundle`オプションにバンドル名を指定します。


### plugin chaining

```
[SassPlugin(), CSSPlugin()]
```
こんな感じでプラグインを配列で指定すると、まず`SassPlugin`を通してから`CSSPlugin`に渡す、といったことができます。
plugin chaining という機能名らしいです。


### `homeDir`フォルダ配下のソースしか読んでくれない

例えば以下のようなフォルダ構成で`index.js`がエントリポイントであるとき、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/1dae9e55-7f45-1397-9e77-878bf64dce07.png)

```js:fuse.js
Sparky.task('config', () => {
  fuse = new FuseBox({
    homeDir: 'src/javascripts/',

...

  app = fuse.bundle('bundle').instructions('> index.js')
})
```

と設定すると、`homeDir`に指定した`src/javascripts`配下の`hoge.js`や`hage.js`は読み込めますが、その他の`src/stylesheets`や`src/assets`配下にあるファイルは読み込めません。(バンドルは正常に終了するが、ブラウザで実行時エラーが発生する)

この場合、

```js:fuse.js
Sparky.task('config', () => {
  fuse = new FuseBox({
    homeDir: 'src/',

...

  app = fuse.bundle('bundle').instructions('> javascripts/index.js')
})
```

このように、`homeDir`に指定するのは`src`までとすることで、ブラウザで正常に読み込めるようになります。
私はこの問題で三時間くらい悩みました……。



## 参考

- [FuseBox × Babel ~ Sparky で Gulp のようにタスクを定義する](https://tech.recruit-mp.co.jp/front-end/post-12727/)