stimulus-rails gemを読み解く

# はじめに

私は、RailsでWebアプリを開発しているプログラマーです。
Webアプリで、インタラクティブな使用感を出したい時は、Hotwireを使っています。
Hotwireとは、レスポンスのHTMLでインタラクティブな使用感を実現するアプローチです。
このアプローチの実現は、TurboとStimulusによりできます。

Turboは、HTTPリクエストを自然に呼ぶだけで画面をインタラクティブに変更できるように設計されたライブラリです。
Stimulusは、HTMLを拡張するように設計されたJavaScriptフレームワークです。

どちらも違和感なく使えています。
これは開発者の設計が良いからでしょう。

このような使い勝手が良いものは、どのように作られているのだろうか？
気になったので、コードを１から読み解いていくことにしました。

RailsでStimulusを使いやすいようにするgemがあります。
手始めに、そのgemのコードを１から読み解きます。

# stimulus-rails gem

[stimulus-rails](https://github.com/hotwired/stimulus-rails) は、Stimulusを気軽に使えるようにするgemです。
stimulus-rails gemは以下の機能を持ちます。

- Stimulus インストーラー
- Stimulus ジェネレーター

このそれぞれの機能がどのようにして成り立っているか読み解きます。

## Stimulus インストーラー

Stimulus インストーラーは、RailsアプリのJavaScriptの管理方法に適したStimulus環境を整えてくれるものです。
Railsアプリに　stimulus-rails gemをインストールして、`bin/rails stimulus:install` を実行すると、インストーラーは以下のことをします。

1. JavaScriptの管理方法を判別する
2. その結果に応じたStimulus環境を構築する

これらのことは、以下ファイルにより行われています。
https://github.com/hotwired/stimulus-rails/blob/main/lib/tasks/stimulus_tasks.rake

上記それぞれの仕組みを読み解きます。

### JavaScriiptの管理方法を判別

stimulus-rails gemでは、JavaScriptの管理方法として以下を想定しています。
- import maps
- JavaScript bundler

管理方法の判別は、以下のコードで行なっています。

```rb:stimulus_tasks.rake
namespace :stimulus do
  desc "Install Stimulus into the app"
  task :install do
    # import maps
    if Rails.root.join("config/importmap.rb").exist? 
      Rake::Task["stimulus:install:importmap"].invoke
    # JavaScript bundler　on bun
    elsif Rails.root.join("package.json").exist? && Stimulus::Tasks.using_bun?
      Rake::Task["stimulus:install:bun"].invoke
    # JavaScript bundler on yarn
    elsif Rails.root.join("package.json").exist?
      Rake::Task["stimulus:install:node"].invoke
    else
      puts "You must either be running with node (package.json) or importmap-rails (config/importmap.rb) to use this gem."
    end
  end
```

このコードから明らかなように、ファイルの有無によってJavaScriptの管理方法を判別しています。
`config/importmap.rb`があったら、import mapsを使っていると判別しています。
Railsでimport mapsを使う際、[importmap-rails](https://github.com/rails/importmap-rails) gemが使用されます。
`config/importmap.rb`は、このgemで使われるファイルです。
なので、このファイルがあれば、import mapsを使っていると判別しています。

JavaScript bundlerを使っているかは、`package.json`の有無により判別しています。
JavaScript bundlerに関しては、JavaScript パッケージマネージャーとして何を使っているかも判別しています。
`Stimulus::Tasks.using_bun?　== true`の時に、パッケージマネージャーとして[bun](https://bun.sh/)を使っていると判別しています。
`bun.config.js`があったら、`using_bun? == true`になります。
`bun.config.js`がない場合は、パッケージマネージャーとしてyarnを使っていると判別しています。

### Stimulus環境を構築

上記の判別結果に則したRakeタスクを実行し、Stimulus環境を構築します。
例えば、import mapsの場合は、`Rake::Task["stimulus:install:importmap"].invoke`を実行します。
これにより実行されるタスクは以下で、`run_stimulus_install_template`メソッドを実行しているだけです。

```rb:stimulus_tasks.rake
namespace :install do
  desc "Install Stimulus on an app running importmap-rails"
    task :importmap do
      Stimulus::Tasks.run_stimulus_install_template "stimulus_with_importmap"
    end
```

`run_stimulus_install_template`メソッドは、[Rails アプリケーションテンプレート](https://railsguides.jp/rails_application_templates.html)を適用します。

```rb:stimulus_tasks.rake
module Stimulus
  module Tasks
    extend self
    def run_stimulus_install_template(path)
      system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb",  __dir__)}"
    end
```

例えば、import mapsの場合は以下のテンプレートが実行され、import mapsに適したStimulus環境を構築します。
https://github.com/hotwired/stimulus-rails/blob/main/lib/install/stimulus_with_importmap.rb


## Stimulus ジェネレーター

Railsといえば、ジェネレーターです。
Stimulusにも、当然ジェネレーターがあります。
例えば、`bin/rails g stimulus hello` を実行すれば、以下のファイルが作られます。

```js:app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="hello"
export default class extends Controller {
  connect() {
  }
}
```

ジェネレーターを定義しているファイルは以下です。
https://github.com/hotwired/stimulus-rails/blob/main/lib/generators/stimulus/stimulus_generator.rb

ジェネレーターの肝となっているのは、`copy_view_files` メソッドです。

```rb:stimulus_generator.rb
class StimulusGenerator < Rails::Generators::NamedBase # :nodoc:
  source_root File.expand_path("templates", __dir__)

  class_option :skip_manifest, type: :boolean, default: false, desc: "Don't update the stimulus manifest"

  def copy_view_files
    @attribute = stimulus_attribute_value(controller_name)
    template "controller.js", "app/javascript/controllers/#{controller_name}_controller.js"
    rails_command "stimulus:manifest:update" unless Rails.root.join("config/importmap.rb").exist? || options[:skip_manifest]
  end
```

下記テンプレートをもとにして、ファイルを作成します。
https://github.com/hotwired/stimulus-rails/blob/main/lib/generators/stimulus/templates/controller.js.tt

JavaScriptの管理方法による違いは1つだけあります。
JavaScript bundlerを使っている時だけ、`stimulus:manifest:update`を実行しています。
これにより、`app/javascript/application.js`が更新されます。
Stimulusジェネレーターを使って作成したもの使えるようにするためです。

# おわりに

コードを読むことで、JavaScriptの管理方法に適した環境をいい感じに整えてくれていることがわかりました。
環境の違いで色々とやらないといけないことが増えると、使うのが嫌になってきます。
Railsが重んじる「設定より規約」を体現している良いgemです。