Sinatra アプリで Sass の処理を行う一手法

もっと良いやり方があるかもしれないけど。

# 動機

Sinatra で Sass を使うには，

```rb
get "/hoge.css" do
  sass :hoge
end
```

みたいな感じ書いてやればいいのだが，この `sass` メソッドを使う方法は [sass](https://rubygems.org/gems/sass) gem もしくは [sassc](https://rubygems.org/gems/sassc) gem を使うことが前提になる（たぶん）。
両 gem は既に開発を終了しており，イマドキの Sass 仕様（`@use` とか）には対応していないので，新規プロジェクトでは使いたくない。

Ruby でイマドキの Sass を使うには，[sass-embedded](https://rubygems.org/gems/sass-embedded) gem を採用するのがよいと考えている。

Sinatra では，CSS など静的なもの（動的生成しなくよいもの）は `public` ディレクトリーに入れておけばよく，上記のような `get` うんぬんというコードはそもそも要らない（開発中だけコンパイルできればよい）。ただ，Sinatra アプリの開発中は，サーバーを起動したまま Sass に加えた変更がすぐに反映されるようにしたい。

これが本記事の動機。

# 方針

Sass ファイルの変更を監視して，変更が加えられた時点（Sass ファイルをエディターで変更して保存した時点）でコンパイルし，`/public` に収めることにした。
Sinatra アプリの動作とは完全に独立。

Sass のコンパイルには，上述した [sass-embedded](https://rubygems.org/gems/sass-embedded) を使うことにする。

ファイルの（変更の）監視は定番の gem である [listen](https://rubygems.org/gems/listen) を使うことにする。

## sass-embedded の紹介

sass-embedded の使い方は超簡単で，

```rb
require "sass-embedded"

path = "foo/bar/baz.css.sass"
css = Sass.compile(path, style: :compressed).css
```

でコンパイル後の CSS が得られる。
`style: :compressed` はギチギチに圧縮された CSS を得るためのオプション。これが無い場合（つまり既定では）人間に見やすい CSS が得られる。
`compressed` にした場合，先頭に BOM（バイトオーダーマーク）が付きうることに注意しよう。
詳しくは以下の拙文をどうぞ：
[Ruby で Sass るには - Qiita](https://qiita.com/scivola/items/a7610ca1638119c31d8d)

Sass ファイルが Sass 記法（インデント記法）なのか SCSS 記法なのかはファイルパスの拡張子で判断してくれる。

## listen の紹介

listen の使い方も超簡単で，大雑把にいうと

```rb
require "listen"

listener = Listen.to("監視したいディレクトリー") do |modified, added, removed|
  # modified は変更が加わったファイルのパスの配列
  # added は新規追加されたファイルのパスの配列
  # removed は削除されたファイルのパスの配列
end
listener.start
```

と書いて，ブロック内で何か処理してやるだけ。

なぜ「ファイルのパス」ではなく「ファイルのパス**の配列**」かというと，複数のファイルが極めて近いタイミングで変更なり追加なりされると，複数ありうるから。
まあ人手で編集しているときはまずそういうことは起こらないので，「`modified`, `added`, `removed` のどれか一つだけが要素数 1 で，他は空配列」になるだろう。

なお，`listener.start` をしてもリスナーを起動するだけで，この式の評価自体は一瞬で終わってしまう。したがって，上記のような内容だけのスクリプトを実行しても，スクリプト自体が一瞬で終わり，当然，ファイルの監視・処理も終わる。
なので，上記のコードを独立のスクリプトで実行させる場合は `listener.start` のあとに `sleep` を置いてスクリプトが終了しないようにする必要がある。

しかし，Sinatra アプリのコードに組み込んでおけば，Sinatra アプリが動いている間はリスナーが生き続けるので，その必要はない。

ところで，特定の拡張子のファイルだけを監視対象にすることもできる。`Listen.to` の引数として，以下のように `:only` を渡せばよい。

```rb
Listen.to("path/to/directory", only: /\.(?:sass|scss)\z/)
```

これで，ファイル名の末尾が `.sass` か `.scss` で終わっているものだけが監視される。

# 作譜

ファイルツリーはこんな感じ：

```
├── config.ru
├── public
│   └── style.css
├── Gemfile
├── views
│   └── index.erb
└── assets
    └── style.css.sass
```

`assets/style.css.sass` が自動的にコンパイルされて `public/style.css` として格納されるようにする。

以下，ファイルの中身を示す。

```rb:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

gem "listen"
gem "sass-embedded"
gem "sinatra"
gem "webrick"
```

```erb:views/index.erb
<!DOCTYPE html>
<link href="style.css" rel="stylesheet">
<h1>Hello</h1>
```

```sass:assets/style.css.sass
h1
  color: red
```

```rb:config.ru
# frozen_string_literal: true

require "bundler"
Bundler.require

ROOT_DIR = Pathname(__dir__)
ASSETS_DIR = ROOT_DIR / "assets"
PUBLIC_DIR = ROOT_DIR / "public"

# 引数に与えたパスの Sass ファイルを処理して CSS ファイルを
# public の下に収める
def compile_sass(source_path)
  source_path = Pathname(source_path)
  source_rel_path = source_path.relative_path_from(ASSETS_DIR)
  css_path = PUBLIC_DIR / source_rel_path.sub_ext("")
  css_path.dirname.mkpath
  css_path.write Sass.compile(source_path, style: :compressed).css
  puts "[compile] #{source_rel_path}"
end

listener = Listen.to(ASSETS_DIR, only: /\.(?:sass|scss)\z/) do |modified, added, _removed|
  [*modified, *added].each do |path|
    compile_sass(path)
  end
end
listener.start

# 起動時にもコンパイル
ASSETS_DIR.glob("**/*.css.{sass,scss}") do |source_path|
  compile_sass(source_path)
end

# Sinatra アプリ
class MyApp < Sinatra::Base
  get "/" do
    erb :index
  end
end

run MyApp
```

これで

```terminal
bundle exec rackup
```

とすれば Sinatra アプリが起動し，http://localhost:9292/ にアクセスすれば赤い「Hello」が表示される。

`config.ru` は本来はアプリ本体を記述するファイルではないが，本記事では最小限のファイル数で手法を示すためにこうした。

なお，Sass の処理が必要になるのは development 環境だけなので，Sass まわりのコードは

```rb
if settings.development?

end
```

で囲っておくとよい。

# おわりに

「方針」節に書いたように，Sass を処理するところは Sinatra とは全く関係ない。
だから，Sinatra アプリに限らず，ローカルでウェブサーバーを動かしながら，Sass ファイルの編集を行なって，それがただちに反映されるようにしたい場面なら何にでもこの手法が使える。