Phoenix 1.6でSCSSを使いたい

[phoenixframework/esbuild]: https://github.com/phoenixframework/esbuild
[flatpickr]: https://github.com/flatpickr/flatpickr
[CargoSense/dart_sass]: https://github.com/CargoSense/dart_sass
[phoenixdiff.org]: https://www.phoenixdiff.org/?source=1.5.13&target=1.6.2
[Bootstrap]: https://getbootstrap.com/
[SCSS]: https://sass-lang.com/
[Phoenix.Endpoint watcher]: https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#module-runtime-configuration

先日あるPhoenixアプリをPhoenix 1.6に更新していたときに、[SCSS]をどのように設定すればよいのか戸惑ったのでメモします。ちなみになぜ[SCSS]が必要だったかというと、[Bootstrap]を[SCSS]でカスタマイズしていたからです。また、Tailwindは使わない方針なのです。

[English version](https://dev.to/mnishiguchi/elixir-phoenix-16-esbuild-scss-1go7)

## 結論

- 必要な知識はすべて[Phoenix 1.6 Asset Management ドキュメント](https://hexdocs.pm/phoenix/asset_management.html)に書いてある。
- 一番シンプルなやり方は、[phoenixframework/esbuild]でJS、[CargoSense/dart_sass]でSCSSの分業体制。
- [SCSS]を使いたいときは、[phoenixframework/esbuild]と[CargoSense/dart_sass]とを両方使ったら良い。ただし、闇雲にドキュメントに書いてあるコードをコピーせず、事前にどのような手順でアセットを処理したいのかしっかり考えた上で設定することが大事。

## [phoenixframework/esbuild]

- 新規作成のPhoenix 1.6アプリでは何もしなくても設定済。
- JSをビルドして、まとめてくれる。（生成物：`priv/static/assets/assets/app.js`）
- CSSがimportされている場合は、CSSをビルドして、まとめてくれる。（生成物：`priv/static/assets/assets/app.css`）
- esbuildの実行プログラムは[phoenixframework/esbuild]さえ正しく設定されていれば、自動的にインストールされる。
- [SCSS]を使いたい場合な、以下のいずれかが必要になるそうです。
  - [CargoSense/dart_sass]
  - esbuild plugins（これは[phoenixframework/esbuild]では無理っぽい）
- 開発時、 [Phoenix.Endpoint watcher]を用いることにより、アセットに変更があるたびに自動でビルドされるようにすることができます。
- 本番用にはデプロイの都度`mix assets.deploy`コマンドでコンパイルをすることになると思います。
- たまに古い生成物が悪さをする場合があります。挙動が変だったら`priv/static/assets/`を確認して、不要なファイルを削除してください。

**esbuildの設定**

3つのファイルに渡って設定されるようです。

- `mix.exs`
  - elixir関連
- `config/config.exs`
  - esbuild関連
  - 実行時の引数を指定
    - 使用したいesbuildの実行プログラムのバージョン
    - ビルドしたいファイル
    - ビルド生成物を置くディレクトリー
    - など
- `config/dev.exs`
  - [Phoenix.Endpoint watcher]関連（開発用）

[phoenixframework/esbuild]の基本的な使い方については思っていたより簡単でした。ただ、初期設定のままだと[SCSS]が使えないのです。
どうしても[SCSS]が使いたいときには[CargoSense/dart_sass]が便利です。

## [CargoSense/dart_sass]

- [SCSS]を[phoenixframework/esbuild]と同じ要領で設定できる。
- [phoenixframework/esbuild]同様、3つのファイルに渡って設定される。
  - `mix.exs`
  - `config/config.exs`
  - `config/dev.exs`

### 注意点

最初、何も考えずに[CargoSense/dart_sass]のドキュメントに書いてあるコードをそのままつかっていたのですが、それでは正しくイゴかない場合があることが判明しました。別にドキュメントが悪いわけではなく、自分がどのように自分のアセットを処理したいのかの方針を明確する必要があるのです。
例えば、[phoenixframework/esbuild]と[CargoSense/dart_sass]の両方のCSSビルド生成物はの行き先が`priv/static/assets/app.css`となっていると、JSファイルにCSSがimportされている場合に競合して同じファイルを上書きすることになってしまうのです。これに気付くまで時間を燃やすことになります。

最初はいろいろゴニョゴニョしようと試行錯誤してしてましたが、結局[phoenixframework/esbuild]でJS、[CargoSense/dart_sass]でSCSSの分業体制がベストだという結論に達しました。一番簡単で確実です。

例えば、以下のような方針が考えられます。

- [phoenixframework/esbuild]ではCSSを一切取り扱わない。
- [CargoSense/dart_sass]の成果物のファイルを`app.css`以外の名前にすると、万一[phoenixframework/esbuild]がCSSを生成しても競合しないので、デバグしやすいかもしれません。

そして、HTMLテンプレートでそのファイルを参照するようにします。

```diff_html:lib/my_app_web/templates/layout/root.html.heex
-  <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
+  <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/from_scss.css")}/>
   <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
```

例えば、`config/config.exs`はこんな感じになりました。

```elixir:config/config.exs
...

# Configure esbuild (the version is required)
config :esbuild,
  version: "0.14.1",
  default: [
    args: [
      "js/app.js",
      "--bundle",
      "--target=es2016",
      "--outdir=../priv/static/assets",
      "--external:/fonts/*",
      "--external:/images/*"
    ],
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

# https://github.com/CargoSense/dart_sass
config :dart_sass,
  version: "1.44.0",
  default: [
    args: [
      "scss/index.scss",
      "../priv/static/assets/from_scss.css" # デバグしやすいよう`app.css`以外の名前にする
    ],
    cd: Path.expand("../assets", __DIR__)
  ]

...
```

## [phoenixdiff.org]

古いPhoenixをPhoenix 1.6更新したい場合には[phoenixdiff.org]が便利です。差分が確認できます。

:tada::tada::tada: