Webpackerのディレクトリ構成をRailsから切り離す形に変える方法

執筆時点で[Webpacker](https://github.com/rails/webpacker)はRailsにてプロジェクトを開始すると、

- `app/javascript/packs/`

以下のファイルを見にいきます。そして、このディレクトリ以下の認識できる**全て**のファイルをWebpackのentryとして吐き出します。

本記事ではこのディレクトリ構成を変え、さらにentryとなるファイルを１つにし、複数のファイルを吐き出さないように書き換えてみようと思います。

## バージョン情報

- Webpacker 1.2
- Rails 5.1.0

## 新しいディレクトリ構成

新しい構成では`frontend`以下にファイルを配置してみます。

```
frontend
├── entry
├── javascripts
└── stylesheets
```
Webpackのentryとするのは`frontend/entry/application.js`です。少し違和感のある構成ですが、現状のWebpackerが吐き出すファイルをあまり書き換えないようにするとこうすべきかと思います。

## Webpackerの設定ファイルなどを書き換える

次に、Webpackerが生成した設定ファイルを書き換えていきます。

### 1. `paths.yml`を修正する

`config/webpack/paths.yml`を書き換えます。

```diff:paths.yml
default: &default
  config: config/webpack
- entry: packs
+ entry: entry
  output: public
  manifest: manifest.json
  node_modules: node_modules
- source: app/javascript
+ source: frontend
  extensions:
    - .coffee
    - .js
    - .jsx
    - .ts
    - .vue
    - .sass
    - .scss
    - .css
    - .png
    - .svg
    - .gif
    - .jpeg
    - .jpg

development:
  <<: *default

test:
  <<: *default
  manifest: manifest-test.json

production:
  <<: *default
```

### 2. `shared.js`を書き換える

Webpackerの生成する設定ファイルは、`config/webpack/shared.js`を共通ファイルとしてdevelopment環境やproduction環境によって設定を変えています。

今回は`shared.js`を書き換えることによって、entryを単一のファイルだけにしてみます。

```diff:shared.js
// Note: You must restart bin/webpack-dev-server for changes to take effect

/* eslint global-require: 0 */
/* eslint import/no-dynamic-require: 0 */

const webpack = require('webpack')
const { basename, dirname, join, relative, resolve } = require('path')
const { sync } = require('glob')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const ManifestPlugin = require('webpack-manifest-plugin')
const extname = require('path-complete-extname')
const { env, paths, publicPath, loadersDir } = require('./configuration.js')
const extensionGlob = `**/*{${paths.extensions.join(',')}}*`
-const packPaths = sync(join(paths.source, paths.entry, extensionGlob))

 module.exports = {
+  entry: {
+    application: resolve(`${paths.source}/${paths.entry}/application.js`)
+  },

-  entry: packPaths.reduce(
-    (map, entry) => {
-      const localMap = map
-      const namespace = relative(join(paths.source, paths.entry), dirname(entry))
-      localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry)
-      return localMap
-    }, {}
-  ),
-
  output: {
    filename: '[name].js',
    path: resolve(paths.output, paths.entry),
    publicPath
  },

  module: {
    rules: sync(join(loadersDir, '*.js')).map(loader => require(loader))
  },

  plugins: [
    new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
    new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'),
    new ManifestPlugin({ fileName: paths.manifest, publicPath, writeToFileEmit: true })
  ],

  resolve: {
    extensions: paths.extensions,
    modules: [
      resolve(paths.source),
      resolve(paths.node_modules)
    ]
  },

  resolveLoader: {
    modules: [paths.node_modules]
  }
}
```

これで`frontend/entry/application.js`のみがentryとして読み込まれるようになりました。

### 3. `.gitignore`を修正する

最後に、entryが変わったので`.gitignore`を修正します。

```diff:.gitignore
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
#   git config --global core.excludesfile '~/.gitignore_global'

# Ignore bundler config.
/.bundle

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

/node_modules
/yarn-error.log

.byebug_history
+/public/entry
-/public/packs
/node_modules

vendor/bundle
```
## 動作確認

それでは動作確認をしてみましょう。以下のファイルを書き換えて表示を確認してみます。

- `frontend/javascripts/application.js`
- `frontend/stylesheets/application.scss`

```js:application.js
/* eslint no-console:0 */
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
//
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
import 'stylesheets/application'
import 'javascripts/hello_react.jsx'

console.log('Hello World from Webpacker')
```

```scss:application.scss
body {
  background: red;
}
```

WebpackerではmoduleのResolvingの設定が書かれているので、相対パスなど気にしなくて良いです。

- [Resolving - Webpack](https://webpack.github.io/docs/resolving.html)

```js:shared.js
  resolve: {
    extensions: paths.extensions,
    modules: [
      resolve(paths.source),
      resolve(paths.node_modules)
    ]
  },
```

`bin/webpack-dev-server`を実行すると、manifestファイルが`puclic/entry`以下に生成されていることが確認できるはずです。

```js:manifest.js
{
  "application.css": "http://localhost:8080/entry/application.css",
  "application.css.map": "http://localhost:8080/entry/application.css.map",
  "application.js": "http://localhost:8080/entry/application.js",
  "application.js.map": "http://localhost:8080/entry/application.js.map"
}
```

あとは`javascript_pack_tag`でjsファイル、`stylesheet_pack_tag`でcssファイルを読み込むだけです。

```erb
<%= javascript_pack_tag 'application' %>
<%= stylesheet_pack_tag 'application' %>
```

適当なページを開くと、多分赤くなっていると思います。

## さいごに

Webpackerが出た当初と比べると、CSSの読み込みなどがいい感じになっているので、ちょっと書き換えるとより快適になる気がしました。

といっても、もちろん単一ファイルを読み込む設定はこれがベストではないと思うので、もっといい方法があれば教えてください:smile: