2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails の ERB にも Biome みたいな書き心地がほしかったので erbfmt を作った

2
Posted at

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_tagvideo_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_withlink_to doturbo_frame_tag docontent_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 は、安全側に倒して保持する。

pretextareascriptstylesvgmathcontenteditable のような、空白や中身の意味が変わりやすい領域も保護対象だ。

たとえば 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 initerbfmt.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 ほしい」

と思ったことがあれば、ぜひ試してほしい!

2
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?