webpackerからapp/assets内のリソースを共用する

こんにちは、はなくらです :raised_hands:
今回は **Webpackerのsass-loaderからRailsの `app/assets` 以下のリソースを利用する** という:cross:闇:cross:を生成する話をします:smiling_imp: 

## Q. なんでそんな不毛な事してるの？🙃 
A.`app/assets/stylesheets` に定義してある変数とコンポーネントをWebpacker上から使いたかったんだ:innocent: 

**`image-url()`などのないsass**を読むだけなら相対パスでimportを頑張ればよいのですが、`app/assets`以下のsassでは
`image-url` や `font-url` という少し厄介なsass関数がいます:rolling_eyes: 
これらの関数が使われているsassファイルを**Sprockets / Webpackerのどちらからimportされても動くようにする**というのが今回のぽはなしです:sleeping: 

## ✋ 手順 
1. `node-sass`上で`image-url`/`font-url`関数を使えるようにする:innocent: 
2. Sprocketsによるコンパイル時と同じパスでリソースを解決できるようにする :sunglasses:
3. file-loaderでリソースを読み込めるようにする :file_folder: 

## 🔨 解説
### 1. `node-sass`上で`image-url`/`font-url`関数を使えるようにする
`node-sass`上で読み込まれたsassには`image-url` や `font-url`などの関数がありませんが、[node-sassの機能](https://github.com/sass/node-sass#functions--v300---experimental)で、JSの関数をsassの世界へ公開する事ができます。
今回は`node-sass`用の`image-url` / `font-url`などの関数を予め定義してあり、パス解決を自前で行うことが出来る[`node-sass-asset-functions`](https://npmjs.com/package/node-sass-asset-functions)というモジュールを利用します。


```javascript:config/webpack/loaders/sass.js
const assetFunctions = require('node-sass-asset-functions')
const assetFuncs = assetFunctions();

module.exports = {
  test: /\.(scss|sass|css)$/i,
  use: [
      // ...
      {　loader: 'sass-loader',　options: {
          // 生成した関数をnode-sassへパスする
          functions: assetFuncs,
      } }
      // ...
    ]
}
```

まずこれで`image-url`などの関数が使えるようになります。

### 2. Sprocketsによるコンパイル時と同じパスでリソースを解決できるようにする

`node-sass-asset-functions`は第1引数にオプションを渡すことができ、このオプションの`asset_cache_buster`プロパティにパス解決を行う関数を渡すことで、`url(path)`に埋め込まれるpathを自由に書き換えることが出来ます。
この関数内でリソースを**ファイルシステム上の絶対パス**へ変換します。
（相対パスでもいいかもしれないんだけど、どこが基点パスになるのか考えるのが面倒）

```javascript
const assetFuncs = assetFunctions({
  asset_cache_buster(filePath, realPath, done) {
    // sass上で `image-url('path/to/image.png')` されると
    // filePathに `images/path/to/image.png` のようなパスが入ってくるので、ファイルシステム上の絶対パスへ解決する
    const { pathname } = url.parse(filePath);
    done({ path: path.join(assetsPath, pathname) });
  },
});
```

さらに、`app/assets/stylesheets`内のスタイルシートをimport出来るようにnode-sassへ`includePaths`オプションを渡します。

```javascript
const stylesheetsPath = join(process.cwd(), 'app/assets/stylesheets')

// ...

module.exports = {
  test: /\.(scss|sass|css)$/i,
  use: [
      // ...
      {　loader: 'sass-loader',　options: {
          // `app/assets/stylesheets` へのパスを渡す
          includePaths: [stylesheetsPath],
          functions: assetFuncs,
      } }
      // ...
    ]
}
```

これでリソースとスタイルシートのパスがSprockets上でのコンパイルと変わらずに解決されるようになります。


### 3. file-loaderでリソースを読み込めるようにする
最後に`file-loader`を設定することでwebpack上から`app/assets`以下のリソース郡を読み込めるようになります。
image-urlなどをファイルシステムの絶対パスに変換されたものが、file-loaderで読み込まれ、webpackでバンドルされます。

```javascript:config/webpack/loaders/file.js
const path = require('path');

module.exports = {
  test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i,
  use: [{
    loader: 'file-loader',
    options: {
      // webpacker.yml のentryと同じにする。 publicPathは/で囲わないと、urlの結合がおかしくなるので注意
      // （ `url('packsapp/assets/...')` みたいになるし、相対パスだと実行時にちゃんと解決されるかわからん）
      publicPath: '/packs/',
      name: process.env.NODE_ENV === 'production' ? '[path][name]-[hash].[ext]' : '[path][name].[ext]',
    },
  }],
};
```

## 最終型✨
あとはお好みでExtractTextPluginとかをどうぞ！:golfer: 

``` javascript:config/webpack/loaders/sass.js
const assetFunctions = require('node-sass-asset-functions')
const path = require('path')
const url = require('url')

const assetsPath = path.join(process.cwd(), 'app/assets')
const stylesheetsPath = path.join(assetsPath, 'stylesheets')

const assetFuncs = assetFunctions({
  asset_cache_buster: (filePath, realPath, done) => {
    const { pathname } = url.parse(filePath);
    done({ path: path.join(assetsPath, pathname) });
  }
});

module.exports = {
  test: /\.(scss|sass|css)$/i,
  use: [
    {
      loader: 'css-loader',
      options: { modules: true }
    },
    { loader: 'resolve-url-loader' },
    {
      loader: 'sass-loader',
      options: {
        includePaths: [stylesheetsPath],
        functions: assetFuncs,
      }
    }
  ]
}
```

----

古い資産を限界まで使い倒したい限界オタクにぽすすめです:relaxed: