Rails の Credentials を Webpacker で管理するフロント側に共有したい

賛否ありますが、Railsにベッタリの開発をしていると、なるべくRailsに寄せて考えたくなることがあります。

そんな

- Webpacker 使ってるプロジェクト
- 環境によって変わる値を環境変数ではなく Credentials にまとめたい。分散させたくない。
- フロントエンドでも使いたい。たとえば Firebase の Project ID や API Key のようなDev,Stg,Prod環境ごとに違って、かつフロントエンドコードに含めて問題ないもの。

そんなときの話です。

- webpack 4.41.5
- webpack-dev-server 3.10.2
- webpacker 4.2.2
- Rails 6.0.1

## Webpacker::Compiler.env は bin/webpack-dev-server を使うとうまくいかない

こんな Credentials があったとき

```yaml
# bundle exec rails credentials:edit --environment=development
firebase:
  project_id: FIREBASE_PROJECT_ID
  api_key: FIREBASE_API_KEY
  auth_domain: FIREBASE_PROJECT_ID.firebaseapp.com
```

Rails から Webpacker に環境変数を渡すには `Webpacker::Compiler.env` が使えます。
[Pass custom environment variables to the compiler #691](https://github.com/rails/webpacker/issues/691)

```config/initializers/webpacker.rb
Webpacker::Compiler.env["FIREBASE_API_KEY"] = Rails.application.credentials.dig(:firebase, :api_key)
Webpacker::Compiler.env["FIREBASE_AUTH_DOMAIN"] = Rails.application.credentials.dig(:firebase, :auth_domain)
Webpacker::Compiler.env["FIREBASE_PROJECT_ID"] = Rails.application.credentials.dig(:firebase, :project_id)
```

```app/javascript/src/firebase.js
import * as firebase from "firebase/app";
import "firebase/auth";

// "FIREBASE_PROJECT_ID: FIREBASE_PROJECT_ID"
console.log(`FIREBASE_PROJECT_ID: ${process.env.FIREBASE_PROJECT_ID}`);

const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT_ID
};

firebase.initializeApp(config);

export default firebase;
```

これは、Rails Server にアクセスした際にビルドする場合や、 `assets:precompile` でビルドする場合には動くのですが、 `bin/webpack-dev-server` では動作しません。

困ったことに `bin/webpack-dev-server` を使ったHMRの快適さに慣れると、いちいちF5なんてやってられません。

```shell-session
$ bin/webpack-dev-server
```

```app/javascript/src/firebase.js
import * as firebase from "firebase/app";
import "firebase/auth";

// "FIREBASE_PROJECT_ID: undefined"
console.log(`FIREBASE_PROJECT_ID: ${process.env.FIREBASE_PROJECT_ID}`);

const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT_ID
};

firebase.initializeApp(config);

export default firebase;
```

### Webpacker::Compiler.env はどう使われるか？

#### Webpacker::Compiler.compile

`rails assets:precompile` を実行したり、ビュー中で `javascript_pack_tag` ヘルパーを実行したりすると、 `Webpacker::Compiler.compile` が呼ばれます。（ `javascript_pack_tag` は `webpack-dev-server` を実行していないときのみ。[このへんのコード](https://github.com/rails/webpacker/blob/master/lib/webpacker/manifest.rb#L42)による ）

`Webpacker::Compiler.compile` は `Webpacker::Compiler.env` の値を環境変数に設定した上で `bin/webpack` を実行します。

https://github.com/rails/webpacker/blob/6427cdbacd71412be781336277bf60b87957a8a8/lib/webpacker/compiler.rb#L68

#### bin/webpack-dev-server

`bin/webpack-dev-server` を実行したときには、 `Webpacker::Compiler.compile` は呼ばれません。
`config/webpacker.yml` から設定を読み出すなどした後、直接 `node_modules/.bin/webpack-dev-server` を実行します。このとき環境変数として `Webpacker::Compiler.env` を渡します。

https://github.com/rails/webpacker/blob/master/lib/webpacker/dev_server_runner.rb#L64

### Webpacker と環境変数

通常、Webpack では、JavaScriptモジュール中の `process.env.HOGE` の値は未定義値となります。

Webpacker では標準で Webpack の EnvironmentPlugin を使って、これができるようになっています。
Webpacker しか知らないと参照できて当たり前のように感じてしまいますが、これは Webpacker が気を回してくれてたからなんですね。

https://github.com/rails/webpacker/blob/13756f1ea6b3393117878bb339b9dd507b801eb2/package/index.js#L21

https://github.com/rails/webpacker/blob/13756f1ea6b3393117878bb339b9dd507b801eb2/package/environments/base.js#L33

```config/webpack/environment.js
const { environment } = require("@rails/webpacker"); // この environment

// こうすると EnvironmentPlugin があることがわかる
console.log(environment.plugins);

module.exports = environment;
```

この２つの仕組みにより `Webpacker::Compiler.env` に設定した値を JavaScript モジュール内で参照できるようになっています。

## bin/webpack-dev-server ではなぜ Webpacker::Compiler.env が機能しないか

上記

> `bin/webpack-dev-server` を実行したときには、 `Webpacker::Compiler.compile` は呼ばれません。
`config/webpacker.yml` から設定を読み出すなどした後、直接 `node_modules/.bin/webpack-dev-server` を実行します。このとき環境変数として `Webpacker::Compiler.env` を渡します。

の通り、 `bin/webpack-dev-server` でも `Webpacker::Compiler.env` は使っています。
しかしながら、 `assets:precompile` や `javascript_pack_tag` と違い、機能しません。

これは、 `bin/webpack-dev-server` では、Railsを起動しないため `config/initializers/webpacker.rb` に書いたコードは実行されないためです。

## bin/webpack-dev-server でも Webpacker::Compiler.env を使う

### A. `config/initializers/webpacker.rb` を実行する

`bin/webpack-dev-server` に次の2行を追加すると、Railsの初期化が走り、 initializers で `Webpacker::Compiler.env` を設定できます。
しかし「`Rails.application.credentials` にアクセスしたい」だけなのに、これはやり過ぎです。

```rb
require_relative '../config/application'
Rails.application.initialize!
```

### B. `config/initializers/webpacker.rb` と `bin/webpack-dev-server` でコードを共有する

`Rails.application.credentials` はRailsの初期化処理なしでもアクセスできます。

たとえば次のようにして共有できます。

```config/webpacker_env.rb
Webpacker::Compiler.env["FIREBASE_API_KEY"] = Rails.application.credentials.dig(:firebase, :api_key)
Webpacker::Compiler.env["FIREBASE_AUTH_DOMAIN"] = Rails.application.credentials.dig(:firebase, :auth_domain)
Webpacker::Compiler.env["FIREBASE_PROJECT_ID"] = Rails.application.credentials.dig(:firebase, :project_id)
```

```config/initializers/webpacker.rb
require_relative "../webpacker_env"
```

```bin/webpack-dev-server
# 追加
require_relative '../config/application'
require_relative "../config/webpacker_env"
```

## あとがき

がんばって Rails / Webpacker でなんとかするために Webpacker のコード読むぐらいなら Webpacker 捨てて Webpackの流儀でやれ、という気持ちになりましたが、一応動かすことはできました。

設定情報の二重管理をせず、手間をかけず、もっと自然に共有する方法はないものだろうか？