Rails の ERB にも Biome みたいな書き心地がほしかったので erbfmt を作った
Rails は好きだ。
でも、.html.erb を書いている時間が長くなるほど、少しずつ Rails が嫌いになっていく瞬間があった。
理由は Rails そのものというより、ERB の開発体験だ。
Next.js で TSX を書いていると、保存した瞬間に Prettier や Biome が整えてくれる。
長い props も、条件分岐も、ネストした JSX も、だいたい「はいはい、こうですね」という顔で揃えてくれる。
一方で Rails の .html.erb はどうか。
HTML のインデントを直す。
<% if %> を足したら ERB のネストがずれる。
form_with を書いたら横に長くなる。
link_to ... do の中にアイコンを入れたら、HTML と Ruby の境界が急に読みにくくなる。
レビューで「ここインデントずれてますね」と言われる。
直す。
また崩れる。
.html.erb は Rails では今でも普通に使うのに、決定版と言える formatter がずっとない感じがあった。
少なくとも自分は、ERB にもこういう体験がほしかった。
erbfmt --write app/views
これで views がいい感じに整う。
そういうものとして、ERB / HTML+ERB 向け formatter / linter の erbfmt を作っている。
GitHub:
Documentation:
VS Code拡張:
まずは小さい例
たとえばこういう ERB があったとする。
<div><% if user.admin? %><span>Admin</span><% else %><span>User</span><% end %></div>
erbfmt をかけるとこうなる。
<div>
<% if user.admin? %>
<span>Admin</span>
<% else %>
<span>User</span>
<% end %>
</div>
これだけでも、手で直さなくてよくなるとうれしい。
ERB では HTML のネストと Ruby のブロックが同時に出てくる。
<ul><% users.each do |user| %><li><%= user.name %></li><% end %></ul>
こういうものも、HTML と ERB の両方を見ながら整える。
<ul>
<% users.each do |user| %>
<li><%= user.name %></li>
<% end %>
</ul>
「ただの HTML formatter」でも「ただの Ruby formatter」でも足りないところを、ERB テンプレート用に扱う、というのが erbfmt の基本だ。
横に長い HTML タグを折りたたむ
Rails views では、タグの attribute がすぐ長くなる。
<section class="profile-card profile-card--featured" data-controller="profile-card autosave" data-profile-card-user-id-value="<%= user.id %>" aria-labelledby="profile-heading">
<h2 id="profile-heading"><%= user.name %></h2>
</section>
erbfmt は、長い HTML tag を attribute ごとに分ける。
<section
class="profile-card profile-card--featured"
data-controller="profile-card autosave"
data-profile-card-user-id-value="<%= user.id %>"
aria-labelledby="profile-heading"
>
<h2 id="profile-heading"><%= user.name %></h2>
</section>
Biome や Prettier で JSX / HTML を書いていると見慣れた形だと思う。
ERB が attribute の中に入っていても、こういう形を目指す。
<a href="/users/<%= user.id %>" aria-label="<%= user.name %>" data-turbo-frame="_top">Profile</a>
<a
href="/users/<%= user.id %>"
aria-label="<%= user.name %>"
data-turbo-frame="_top"
>
Profile
</a>
custom element や self-closing っぽいタグも、長ければ読みやすくする。
<custom-input name="profile[display_name]" value="<%= user.display_name %>" data-controller="autosave" />
<custom-input
name="profile[display_name]"
value="<%= user.display_name %>"
data-controller="autosave"
/>
<%= ... %> の長い Rails helper も折りたたむ
.html.erb でつらくなりがちなのが、長い Rails helper だ。
<%= link_to "Edit profile", edit_user_path(user), class: "button button--primary", data: { turbo_frame: "_top" } %>
こういうものは横に伸びるとかなり読みにくい。
erbfmt では、安全に分割できる単純な呼び出しは、引数ごとに折りたたむ。
<%=
link_to(
"Edit profile",
edit_user_path(user),
class: "button button--primary",
data: { turbo_frame: "_top" }
)
%>
render も Rails views ではよく長くなる。
<%= render partial: "profile", locals: { current_user: current_user, account: account, selected_status: selected_status } %>
<%=
render(
partial: "profile",
locals: {
current_user: current_user,
account: account,
selected_status: selected_status
}
)
%>
image_tag や video_tag のような helper も対象だ。
<%= image_tag("user-placeholder.png", alt: "User profile image", class: "avatar avatar--large") %>
<%=
image_tag(
"user-placeholder.png",
alt: "User profile image",
class: "avatar avatar--large"
)
%>
<%= video_tag(["intro.mp4", "intro.webm"], controls: true, autoplay: false, class: "hero-video") %>
<%=
video_tag(
["intro.mp4", "intro.webm"],
controls: true,
autoplay: false,
class: "hero-video"
)
%>
このあたりが整うと、Rails views の読み心地がかなり変わる。
form_with ... do も読みやすくする
ERB で一番よく見る長い塊のひとつが form_with だ。
<%= form_with model: user, url: user_path(user), data: { turbo_frame: "profile" } do |form| %><div><%= form.text_field :name %></div><% end %>
erbfmt では、<%= ... do %> のような output do-block も扱う。
<%=
form_with(
model: user,
url: user_path(user),
data: { turbo_frame: "profile" }
) do |form|
%>
<div>
<%= form.text_field :name %>
</div>
<% end %>
link_to ... do も同じだ。
<%= link_to "Edit profile", edit_user_path(user), class: "button button--primary", data: { turbo_frame: "_top", controller: "profile-link" } do %><span>Edit</span><% end %>
<%=
link_to(
"Edit profile",
edit_user_path(user),
class: "button button--primary",
data: {
turbo_frame: "_top",
controller: "profile-link"
}
) do
%>
<span>Edit</span>
<% end %>
Rails views では、form_with、link_to do、turbo_frame_tag do、content_tag do など、HTML と Ruby のブロックが混ざる場面がとても多い。
ここが自動で揃うだけで、かなり「現代的な formatter があるプロジェクト」っぽい体験になる。
<% ... %> の code tag も整える
<%= ... %> だけでなく、出力しない <% ... %> も長くなることがある。
<% tag.div class: "card", data: { controller: "profile" }, aria: { label: "Profile" } %>
<%
tag.div(
class: "card",
data: { controller: "profile" },
aria: { label: "Profile" }
)
%>
長い条件式も、少なくともタグ自体は複数行に逃がせる。
<% if current_user.admin? && feature_enabled?(:new_dashboard) && account.active? %>
<p>Hello</p>
<% end %>
<%
if current_user.admin? && feature_enabled?(:new_dashboard) && account.active?
%>
<p>Hello</p>
<% end %>
<% ... %> が横に伸びて HTML の見通しを壊す、というケースを少しでも減らしたい。
ただし、壊しそうなものは無理に触らない
formatter は賢く見えることより、壊さないことの方が大事だ。
たとえば、こういう inline の並びは改行すると見た目が変わる可能性がある。
<%= form.radio_button :status, "draft" %><%= form.label :status_draft, "Draft" %>
このような隣接した inline ERB output は、安全側に倒して保持する。
pre、textarea、script、style、svg、math、contenteditable のような、空白や中身の意味が変わりやすい領域も保護対象だ。
たとえば script の中では < や > が普通に出てくる。
<script>
if (x < 10) {
console.log("small")
}
</script>
こういうものを HTML tag と誤認して壊さないようにしている。
安全に整えられるものは整える。
危なそうなものは保持する。
実プロジェクトで壊れたら regression test にして潰していく。
erbfmt はそういう方針だ。
使い方
Rails project では Bundler 経由で入れるのがおすすめ。
bundle add erbfmt --group development --require false
bundle exec erbfmt --version
設定ファイルを作る。
bundle exec erbfmt init
フォーマットする。
bundle exec erbfmt --write app/views
CI では --check を使う。
bundle exec erbfmt --check app/views
linter として使う場合は --lint
bundle exec erbfmt --lint app/views
手元でグローバルに試したいだけなら RubyGems から直接入れることもできる。
gem install erbfmt -v 0.2.0
erbfmt --version
設定
erbfmt init で erbfmt.json を生成できる。
{
"$schema": "https://raw.githubusercontent.com/hinamimi/erbfmt/refs/heads/main/docs/schema/erbfmt.schema.json",
"files": {
"includes": ["**/*.html.erb"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"indentHtml": true,
"lineEnding": "lf",
"lineWidth": 80,
"trailingNewline": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
files.includes で対象ファイルを絞れる。
{
"files": {
"includes": [
"app/views/**/*.html.erb",
"!app/views/generated/**"
]
}
}
ディレクトリを渡すと再帰的に探索する。
bundle exec erbfmt --write app/views
linter としても使える
erbfmt は formatter だけではなく linter としても使える。
bundle exec erbfmt --lint app/views
たとえば以下のようなものを検出する。
- 壊れた HTML 構造
- 不正な list / table のネスト
- deprecated な HTML tag
- self-closing HTML tag
- duplicate attribute
- 空の ERB tag
- 未対応の ERB control-flow
lint の出力は、デフォルトでは source excerpt 付きで表示する。
app/views/users/show.html.erb:
error: duplicate HTML attribute `class`
--> line 1, column 32
|
1 | <article class="card" id="one" class="wide"></article>
| ^
1 lint issue found (1 error, 0 warnings).
このあたりも、Biome や Ruff のように「読める diagnostics」に近づけたい。
erbfmtの現状
まだ pre-release でRuby のコードを完全な Ruby AST として解析しているわけではない。
複雑で曖昧な Ruby 式は、無理に書き換えず保持することがある。
また、ERB / HTML のパターンを、全部を最初から完璧に扱えるわけではない。
ただ、実際に使って気になったものは、なるべく小さく直して regression test にしていく方針だ。
Issue も大歓迎!
まとめ
Rails は好きだ。
でも、ERB の開発体験が TSX と比べて弱いせいで、Rails まで嫌いになっていくのはもったいない。
.html.erb に決定版と言える formatter がないなら、作りたい。
Biome みたいに、保存したらだいたい読みやすくなる。
CI で崩れを検知できる。
VS Code でも同じ結果になる。
横に長い Rails helper や HTML tag が、ちゃんと読める形になる。
erbfmt はそういう体験を目指して作っている ERB formatter / linter だ。
GitHub:
Rails の .html.erb を書いていて、
「ERB もそろそろ普通に formatter ほしい」
と思ったことがあれば、ぜひ試してほしい!