メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

松本の地でSass/SCSSの邪悪なアンパサンドを撲滅するために立ち上がった

メドピアCTO室フロントエンドエンジニアの小宮山です、よろしくおねがいします。
趣味はボルダリングとヨガとピラティスです、よろしくおねがいします。

6月某日、長野県松本市の地にて開催されたメドピア開発合宿で取り組んだことについて紹介していきたいと思います。

アンパサンドへのウラミツラミ

タイトルにも挙げたとおり、今回立ち向かったのはSass/SCSSのアンパサンド(&)です。 メドピアのリポジトリはほぼSCSSで統一されているので、この記事ではSCSSの記法ベースでコード例を載せていきます。

アンパサンド記法は、BEMライクなセレクタを書くときによく利用されるのではと思います。
例えばこのようなものです。

.header {
  &__foo {
    color: green;
  }
  &--bar {
    color: blue;
  }
}

アンパサンドを使い、冗長な記述を限りなく減らしたスマートな書き方ですね。書いてるときはDRYなライブ感と共にスタイルを書き進めることができます。

しかしこの書き方には、セレクタの検索可能性が損なわれるという非常に大きな欠点があります。

検索妨害

上述のようなスタイル指定がされたコードに手を入れる場面を想定してみます。変更を加えようとしているファイルはhtmlとjs側です。

html

<div class="header__foo">
  foo
</div>

htmlにはheader__fooというclassを持ったタグがあり、jQueryを使ってセレクタからそのタグを取得して何かの操作をしているとします。

js

$('.header__foo').hide()

何の変哲もない古き良きjQueryコードです。

続いてこの処理をVueで置き換えてみます。細かいところは省略しますが、こんな感じになるのではないでしょうか。

<template>

<div class="header__foo" v-show="show">
  foo
</div>

<script>

Vue.extend({
  data() {
    return { show: false }
  }
})

さくっと置き換えることができ、JavaScript側はもうタグに付けておいたclassを必要としなくなりました。
不要なものは消せるときに消してしまうのが正義です。早速header__fooというclassをタグから消してすっきりさせてしまいましょう。

<div v-show="show">
  foo
</div>

と迂闊に消してしまう人はさすがにいないと信じたいです。JavaScriptから参照していなくても、CSSからclassを参照している可能性は大いに残っています。

このclassに対するCSSを書いた本人なら、このclassがまだ必要であることを覚えているかもしれません。しかし人の記憶は頼りになりませんし、このclassを消していいか迷っているのは何も知らない別人かもしれません。消したいclassがまだどこかで使われているかの判断に迷ったときは、grepするに限ります

どんな結末が待っているかはもう想像がついたかと思います。アンパサンドによる文字列結合で指定されているこのセレクタは、タグに付いているclass名でgrepしても拾われません。

.header {
  &__foo {
    color: green;
  }
}

そして使われていないことを確信し、コード改善と思ってclassを消した開発者には、スタイル崩れという悲劇が待ち受けています。

このようにアンパサンドによってセレクタが文字列結合されていると、タグのidやclassがどこで使われているのか見つける手段が大幅に制限されてしまいます。 地道に探せば見つけられるかもしれませんが、その労力と、不要なセレクタを減らす地道なコード改善が釣り合うのかは微妙なところです。
そして結局は触らぬ神に祟りなし、必要なのかも分からないidやclassは消えることなく未来に向かって増え続けていきます。

purgecss

検索妨害と仕組みは同じですが、アンパサンドによるセレクタ結合はpurgecssによる未使用スタイル削除の妨げとなる可能性があります。

purgecssは非常に強力なものの、セレクタを使っているかの判定は静的解析によって為されています。どこまで正確に判定しているのかまでは精査できていませんが、不要なリスクを回避する意味でも、アンパサンドやスクリプトを用いた文字列結合によるセレクタ生成は避けたほうが無難です。

合宿の地、松本にて

grepを阻害する邪悪なアンパサンドによるセレクタ文字列結合は、すでにメドピアのリポジトリ群に多く入り込んでいました。この現状に立ち向かうべく、アンパサンドを使わない素のセレクタ文字列に変換してしまおうと決意を固めます。

そしてメドピアでは年に数回、2泊3日かけて自由なテーマに取り組むことができる開発合宿を開催しています。 ボリューム的にちょうど良さそうだったこともあり、アンパサンド変換用のCLIツール開発をテーマに決めて取り組むことにしました。

採用言語

CLIツールの開発言語にはRustを選びました。フロントエンド関連なのだからJavaScriptやTypeScriptでいいじゃないかという葛藤はもちろんあったものの、舞台はせっかくの開発合宿です。今の最善でないとしても、興味のある技術にフォーカスしてこそ開発合宿です。普段とは違う言語に四苦八苦したくなるときだってあるんです。

成果物

リポジトリはこちら。 github.com

CLI

まずは当初目的通りCLIツールです。clapというCLIツール作成用便利パッケージを使うと引数解析やヘルプ表示などを簡単に作れます。

USAGE:
    sassruist [FLAGS] <path>

FLAGS:
    -f, --fix        fix original file(s)
    -h, --help       Prints help information
    -V, --version    Prints version information

ARGS:
    <path>    target file or directory path

ファイルを書き換えたりといったCLI特有の機能以外はWebAssembly版と同じ動作なので、デモなどはそちらに引き継ぎます。

WebAssembly

フロントエンド、Rustというキーワードが来たら当然続くのはWebAssemblyでしょう。 冒頭で長々と書いた当初の目的を果たすにはCLIツールさえあれば十分ですが、舞台は開発合宿です。隙あらば気になる技術に全力投球していきます。

成果物

netlifyにデモサイトを用意したのでお試しください。 determined-wescoff-282115.netlify.com

妥協点

成果物発表直後ですが早速妥協点紹介です。というのも正直なところ今回の成果物・・・作った本人から見ても実用レベルには至りませんでした、無念。
以下その理由です。

変換結果

「アンパサンドを置換するだけだし実装なんて簡単だろう」と気楽に作り始めてしまったのが運の尽き、Sassアンパサンド(というよりSassそのもの)は想像以上に手強い相手でした。

まずはこの変換例を御覧ください。

f:id:robokomy:20190716135423p:plain
変換例

みにくい!!!

そうなんです。アンパサンドを解決するにはセレクタのネスト構造を解決する必要があり、そのために記述が複雑になることを避けられませんでした。

ちなみに余談ですが、当初は勢いで走りすぎてこういう変換をしていました。間違い探しとしてお収めください。

f:id:robokomy:20190716135917p:plain
間違い探し

制約

100歩譲ってみにくさは我慢するとしても(diffが死ぬので譲りにくいですが)、仕様的に無視しにくい大きな制約もいくつか残ってしまいました。

特に厳しいのは複数行のセレクタに対応できていないことです。

f:id:robokomy:20190716140516p:plain
🤔🤔🤔

見るも無残な姿になってしまいました。「1行ごとに処理すればいいだろう」という謎の自信に満ちた実装方針により、複数行セレクタへの対応が必要と気付いたときには既に軌道修正が間に合いませんでした。

あと地味にSassを諦めてSCSS専用になっています。妥協点満載です。

CLIとWebAssembly両対応ビルド

気を取り直して、今回一番苦戦した点の紹介です。実は実装を差し置いて、CLIとWebAssemblyの両方にRustコードをビルドすることにかなり苦戦しました。
RustやCargo.toml自体への理解度もあまり深くなく、試行錯誤の末になんとか形になった方法をここで紹介したいと思います。

シンプルなWebAssemblyビルド

RustをWebAssembly化し、ウェブページとして公開する1連の流れ自体はこちらの記事を大いに参考とさせていただきました。

webbibouroku.com

bindgen、wasm-pack、create-wasm-appなど、便利なツールの導くままにWebAssemblyなウェブページを作ることができてしまいます。

RustをWebAssemblyにビルドするだけなら解説通りにwasm-packを使うだけです。しかし今回はもう1つのターゲット(こちらが本命のはずなんですが)としてbinaryにもビルドする必要があります。

異なる依存パッケージ

CLIツールとしてはディレクトリ内のファイルを一気に処理させたかったので、dependenciesにwalkdirというファイル探索用パッケージを追加していました。
ウェブページて使うWebAssemblyにファイル操作は不要なのですが、Cargo.tomlのdependenciesに書かれたパッケージがWebAssemblyビルドのときにも認識され、wasm-pack buildコマンドを実行すると以下のようなエラーを吐かれてしまいます。

error[E0433]: failed to resolve: use of undeclared type or module `imp`
   --> /~~~/.cargo/registry/src/github.com-1ecc6299db9ec823/same-file-1.0.4/src/lib.rs:261:9
    |
261 |         imp::Handle::stdout().map(Handle)
    |         ^^^ use of undeclared type or module `imp`

原因は推測ですが、WebAssemblyにビルドできない何か(walkdirなのでおそらくファイル操作系)が混ざっていると判定されているのだと思います。
その証拠に、Cargo.tomlのdependenciesからwalkdirをコメントアウトすると無事にビルドが成功します。

つまり、WebAssemblyにビルドするときだけ手動でその行をコメントアウトすれば解決します!!!

許されません、これは許されません。ビルドコマンドをただ実行するだけなのに、こんなにも露骨な運用でカバープロセスを挟み込むなんて許されるわけがありません。README.mdにこんなビルド方法の解説を書くこともとてもできません。

環境ごとの依存パッケージ変更

本体のコーディング以上に血眼になって探し、ついにここで答えを見つけました。
修正後のCargo.tomlの書き方はこちらです。

gist.github.com

ポイントはclapwalkdirパッケージに付けたoptional = trueというオプションで、featuresとしてbinを指定したとき(cargo build --feautres bin)だけビルド時の依存関係に含めてくれるようになります。

f:id:robokomy:20190705185245p:plain

大まかな流れは上図のようになっています。WebAssemblyとしてビルドするときは不要なパッケージをwasm-pack buildから除外することに成功しました。そしてCLIとしてmain.rscargo buildするときは、--feautres binを引数に加えることで依存パッケージをしっかりと認識してくれます。
引数追加程度ならREADME.mdに書くこともきっと許されることでしょう。

本当はCLIとしてビルドするときにWebAssemblyでしか使わないパッケージも除外したかったのですが、こちらはwasm-packをさらに解析する必要がありそうで今回は泣く泣く断念しました。こちらの問題にも対応した完全版両対応ビルド設定の模索は今後の課題です。

f:id:robokomy:20190705185931p:plain

成果物やデモを公開するには、やはりウェブブラウザで見れるというお手軽さは魅力的です。本命のCLIツールを作り込みつつ、さくっと触れるデモをウェブで公開したいという贅沢な欲求をWebAssemblyは見事に叶えてくれました。

まとめ

Sass/SCSSの邪悪なアンパサンドへのウラミツラミからスタートし、それっぽく動くけど妥協点満載な解決ツールを作り、WebAssemblyの知見を紹介して締めるという全くまとまりのない記事となってしまいましたが、メドピア開発合宿3日間が見事に集約されています。自分で言うのもあれですが見事に集約されています。

実は人生初の開発合宿というイベントでした。好きなテーマに取り組んでいいという我らがCTO福村の言葉をそのまま受け取り、このように本当に好きなことに取り組むことができた濃密な3日間となりました。これで当初目的だったアンパサンド撲滅も達成できていたら最高だったのですが、残念ながら力及ばず。

しかしなんと、同じく合宿に参加していたフロントエンドエンジニア村上(@pipopotamasu)がstylelint-scssのruleでセレクタのアンパサンド結合を禁止するというスマートすぎるPRをさくっと作って本家のルールに加えてくれました。

github.com

邪悪なアンパサンド勢力の拡大はこうして食い止められたのです。松本の地にて立ち上がっていたのは村上です。僕は何もしていません。やはり持つべきものは仲間です。メドピアではフロントエンドエンジニアの仲間を絶賛募集中です。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html