Ruby スクリプトで Sass を CSS に変換するにはどうするのがよいのか。
Sass の歴史にも最新動向にも詳しくはないので,間違ったことを書いてたらツッコミを入れてほしい。
従来
これまではどうだったのか。
sass gem 時代
かつてその名もズバリ sass という gem があった(今も存在している)。
これは Ruby で書かれた Sass の処理系であった。
そもそも Sass は Ruby が発祥なのだ。
今でも使うことはできる。Gemfile に
gem "sass"
と書いておき1,
require "bundler"
Bundler.require
sass_text = <<~SASS
.foo
color: darken(red, 10%)
margin-top: 10px + 4px
SASS
opts = {syntax: :sass, style: :compressed}
puts Sass::Engine.new(sass_text, **opts).render
# => .foo{color:#c00;margin-top:14px}
のように使える2。実質一行で変換できる。
Sass 記法(インデント記法とも)なら syntax: :sass
とし,SCSS 記法なら syntax: :scss
とする。
この gem には sass
というコマンドが付属しており,このコマンドで変換することもできる。
しかし,Ruby で実装されているため変換速度は遅い。実際,大規模な Sass ファイルの変換では実行速度が問題となっていったようだ(知らんけど)。
そこで,C/C++ で実装された LibSass という高速なライブラリーが誕生した。これはあくまでライブラリーであり,他のプログラムに組み込んで使う。単独で使うものではない。
sass gem は 2019 年に終わった。知らんけど。
sassc gem 時代
その LibSass を Ruby で使えるようにした gem が sassc。
sass gem と同じように使うことができる。
Gemfile に
gem "sassc"
と書いておき,
require "bundler"
Bundler.require
sass_text = <<~SASS
.foo
color: darken(red, 10%)
margin-top: 10px + 4px
SASS
opts = {syntax: :sass, style: :compressed}
puts SassC::Engine.new(sass_text, **opts).render
# => .foo{color:#c00;margin-top:14px}
のように書けばよい。
つまりモジュール名が Sass → SassC と変わっただけで,ほとんど sass gem と同じように使えるようになっている。
ちなみに,Rails で Sass るための sass-rails gem は,以前は sass gem を利用していたが,現在は(sassc-rails gem を経由して)sassc gem を使うようになっている。
ここで話が終わればよかったのだが,LibSass に暗雲が垂れ込める。
C/C++ で Sass の処理系を開発し続けるのは大変であったらしく(?),開発は停滞気味になっていった。
その頃,Sass の処理系としては Dart という言語で書かれた Dart Sass があり,そちらは開発が進んでいた。
そして 2020 年 10 月,ついに LibSass は deprecated になってしまう。
保守は続けるが,新機能の取り込みなどは行わないことが決定。
今後は Dart Sass を使うように,とのこと。
えっ,なに,ちょっ,早過ぎね? Ruby 版やめて LibSass 版を使えって言うから切り替えたばかりなのに。
つか,Dart ナニソレ? Ruby でどうやって使うのさ???
でどうすんの?
個人的には Dart に全く馴染みがない。
「Google が JavaScript の代替として Chrome に組み込んで他のブラウザーにも追随させようとして諦めた言語だっけ?」くらいのあやふやな認識だった。
しかし,Ruby 版が遅いから C/C++ 版を作ったのに Dart で実装したらまた遅くならないのかな(実際 LibSsass より遅いようだがよく知らない)。なんでまた Dart なんだろう。
Sass の新機能が必要なければ現状の sassc gem をずっと使い続ければいいか?とも思ったけどそうでもないようだ。
CSS 標準が進化しているのに LibSass がそれに追随できていないというのがそもそも問題であるらしい。
なんにしても,Sass のコアチーム(?)が公式に LibSass を deprecated にし,Dart Sass を推奨している以上,あえてそれを使わないのは不安だ。
sass-embedded gem
Slack の ruby-jp で相談したところ,Dart Sass を Ruby で使うための sass-embedded という gem があることを教えていただいた。
これが sassc gem の代替になっていくのかどうかは分からないけれど,とりあえず試しに使ってみることにした。
使い方は,sass gem や sassc gem とは違うものの,同様にシンプルで,Gemfile に
gem "sass-embedded"
と書いておいて,
require "bundler"
Bundler.require
sass_text = <<~SASS
.foo
color: darken(red, 10%)
margin-top: 10px + 4px
SASS
params = {
data: sass_text,
indented_syntax: true,
}
# expanded(デフォルト)で出力する場合
puts Sass.render(**params).css
# => .foo {
# color: #cc0000;
# margin-top: 14px;
# }
# compressed で出力する場合
puts Sass.render(**params, output_style: "compressed").css
# => .foo{color:#c00;margin-top:14px}
のような感じでいけた。
(2022-01-15 追記)この書き方は sass-embedded 0.10.0 で deprecated になった。記事末の追記参照。
render
メソッドは CSS テキストを返すわけではなく,Sass::Embedded::RenderResult オブジェクトを返すようだ。
このオブジェクトの css
メソッドで CSS テキストが返る。
Sass テキストを与えるときは上の例のように data
オプションを使い,Sass ファイル(のファイルパス)を与えるときは file
オプションを使う。
出力のスタイルは output_style
オプションで切り替える。
indented_syntax
オプションでインデント構文(Sass 構文)か SCSS 構文かを切り替えるらしい(?)。
output_style
Sass の仕様によれば,output_style
は以下の四つが使えるはず。
expanded
compressed
nested
compact
参考:https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#outputStyle
ところが,どういうわけか "nested"
や "compact"
を与えると
output_style must be one of :expanded, :compressed (ArgumentError)
と例外が発生する。
ここ↓で弾いてた。なぜ?
https://github.com/ntkme/sass-embedded-host-ruby/blob/v0.9.3/lib/sass/embedded/compile_context.rb#L211-L220
BOM がつくことがある
sassc gem では,非 ASCII 文字を含んだ Sass で出力スタイルに compressed
を選ぶと先頭に BOM(Byte Order Mark;U+FEFF)が付く。
参考:Sass の吐き出す BOM に注意! - Qiita
これは sass-embedded gem でも同じだった。
モジュール名は Sass
sass-embedded gem のモジュール名(大元というかルートというか大枠というかの)は,Sass
である。SassEmbedded
とかではない。
https://github.com/ntkme/sass-embedded-host-ruby/blob/v0.9.3/lib/sass.rb#L7
このことは,sass-embedded gem が sass gem と両立しないことを意味する。
もちろん両方いっぺんに使うことなんてふつうは無いわけだけど,たとえばベンチマークテストとかしようと思ったら問題になる。
require
gem のなかには,gem 名と require
すべきものの名前が一致しないものがあるが,sass-embedded gem はどうだろうか。
Bundler を使う限り自動的に require
してくれるので意識する必要はないが,たとえば irb で動作確認したり,ちょこっとした瞬間書き捨てスクリプトなんかでは Bundler を使わずに require
することもあるので,知っておきたい。
結論から言えば,
require "sass-embedded"
でも
require "sass"
でもどちらでもいける。
前者は単に require_relative 'sass'
しているだけ:
https://github.com/ntkme/sass-embedded-host-ruby/blob/v0.9.3/lib/sass-embedded.rb#L4
したがって,sass gem と sass-embedded gem が両方インストールされている環境下では Bundler を使わないと意図しないほうを読んじゃう恐れがある,ということになる。要注意。
Node.js の sass パッケージ
私は Node.js についての理解がかなり浅いのでなんか間違ったことを書くかも。ぜひツッコミを。
Node.js には sass というパッケージがあり,(昔は違ったようだけど)現在は Dart Sass を利用するものとなっている3。
(このことも Slack の ruby-jp で教えていただいた)
これを利用するには,まず Node.js をインストールして4,コマンドラインで
npm install -g sass
とすればいいようだ。
-g
というオプションは,(プロジェクトのローカルではなく)グローバルにインストールする,という意味らしい。こうしておけばどこからでもコマンドラインで sass コマンドが使える。
このコマンド名は sass gem によってインストールされるものと競合してしまうので,注意しよう。
確認はコマンドラインで
sass --version
とすれば
1.43.4 compiled with dart2js 2.14.4
のように表示される。これを見れば,sass gem 版の sass コマンドではないことや,なんかよくわからんけど,Dart Sass ぽいことなどが分かる。
(どうも Dart 言語のコードを JavaScript に変換したものを JavaScript の処理系で実行しているらしいのだがよく知らん。つか JavaScript にしたら Dart よりもっと遅くならんの?)
使い方
ここでは,Ruby から Node.js の sass コマンドを使うことを考える。
Sass テキストを標準入力で与えて,結果を標準出力からもらえばいい。
sass コマンドに,Sass テキストを与えるには,--stdin
オプションを付ければよいようだ。
また,インデント構文(Sass 構文)の Sass テキストを与えるなら --indented
オプションを付ける。付けなければ SCSS 構文とみなされる。
出力スタイルは --style compressed
のようにして与える。デフォルトは expanded
だ。
これを用いる Ruby スクリプトは,IO.popen を用いてこんな感じに書ける:
sass_text = <<~SASS
.foo
color: darken(red, 10%)
margin-top: 10px + 4px
SASS
css = IO.popen("sass --stdin --indented --style compressed", "r+") do |io|
io.write(sass_text)
io.close_write
io.read
end
puts css
# => .foo{color:#c00;margin-top:14px}
現実のコード(とくにライブラリーの中)などでは,sass コマンドの存在確認や,それが Node.js 版であることの確認などを行って,不足なら「どうすればよいか」のメッセージを出したりする必要があるだろう。
出力スタイル
どうやら Node.js 版の sass も出力スタイルは expanded
と compressed
しか指定できないようだ。
compact
を与えると
"compact" is not an allowed value for option "style".
というメッセージとともにヘルプ(使い方)が表示される。
え〜,なんで?
いや,私自身は expanded と compressed しか使わないからいいんだけど,Sass の公式サイトで四つのスタイルが書かれているのに,なんだかなあ。
BOM
こちらも,非 ASCII 文字を含む場合に compressed スタイルで出力すると先頭に BOM が付くようだ。
まとめ
- sass gem は使うべきでなく,コマンド名や require するファイルの名前の競合の問題があるので,アンインストールしておいたほうがいい。
- sassc gem も既に deprecated。
- sass-embedded gem を使うか,Node.js の sass パッケージによる sass コマンドを使う,という代替策が考えられる。
追記(2022-01-15)sass-embedded 0.10.0 での書き方
sass-embedded は,2022 年 1 月 15 日にバージョン 0.10.0 がリリースされた。
このバージョンでは,「sass-embedded gem」の節に書いたような
Sass.render(data: "h1 { color: red }")
といった書き方が deprecated になったようだ。ただし,使うことはできるし,画面に警告は出ない。コードのコメントに @deprecated と書いてある だけのようだ。
render
の代わりに導入されたのが Sass.compile
と Sass.compile_string
で,前者はファイルパスを渡し,後者は Sass テキストを渡すものらしい。
また,Sass テキストの文法は indented_syntax
オプションに真/偽を渡すのではなく,syntax
オプションに :indented
,:scss
,:css
のいずれかを渡すようだ。
出力スタイルは output_style
オプションではなく style
オプションに変わった(値は :expanded
か :compressed
)。
したがって,サンプルコードとしてはこうなる:
require "bundler"
Bundler.require
sass_text = <<~SASS
.foo
color: darken(red, 10%)
margin-top: 10px + 4px
SASS
# expanded(デフォルト)で出力する場合
puts Sass.compile_string(sass_text, syntax: :indented).css
# => .foo {
# color: #cc0000;
# margin-top: 14px;
# }
# compressed で出力する場合
puts Sass.compile_string(sass_text, syntax: :indented, style: :compressed).css
# => .foo{color:#c00;margin-top:14px}