JavaScriptライブラリをGemにしてRubyに組み込む


JavaScriptライブラリのためのRubyGemを作ってみたので、作成手順を順を追いながらまとめます。JavaScriptライブラリのGem作成方法は日本語記事があまりなかったので、参考になれば幸いです。

JavaScriptライブラリのGemは、有名所ですと[jquery-rails](https://github.com/rails/jquery-rails)等です。
今回は、Gemを使うときに下記のようにAsset Pipelineのマニフェストファイルで宣言して読み込むタイプのものについて見ていきます。

```js:app/assets/javascripts/application.js
//= require jquery
```

## 今回作ったGem

[react_rails_modal](https://github.com/kiyodori/react_rails_modal)という[react-rails](https://github.com/reactjs/react-rails)上でモーダルを動かすためのGemを今回作りました。react-railsはRailsにReactを導入するためのものです。

使い方は簡単です。
まずreact-railsをインストールした上で、本Gemをインストールします。

```ruby:Gemfile
gem 'react_rails_modal'
```

```
$ bundle install
```

次にRailsコマンドを実行します。

```
$ rails g react_rails_modal:install
```

これにより、下記が`application.js`に追記され、マニフェストファイルにライブラリの読み込みが宣言されます。

```js:app/assets/javascripts/application.js
//= require react_rails_modal
```

これで[react_rails_modal](https://github.com/kiyodori/react_rails_modal)が読み込まれるようになったので、react-rails上でモーダルが使えるようになります。
モーダルの使い方に関しては[react-railsでモーダルを実装する](http://qiita.com/kiyodori/items/367556b86b0da349493e)にまとめしたのでそちらをご覧ください。

ここから、Gemの作成手順を追いながら、JavaScriptライブラリのためのGemの作成方法を見ていきます。

## 準備

まずGemとbundlerのアップデートをします。

```
$ gem update --system

# bundlerをインストールしていない場合はインストールします
$ gem install bundler

$ gem update bundler
```

### ひな形の作成

Gemのひな形をテストありで作ります。

```
$ bundle gem react_rails_modal -t
```

### gemspec

Gemの基本的な情報をgemspecにまとめます。

```ruby:react_rails_modal.gemspec
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'react_rails_modal/version'

Gem::Specification.new do |spec|
  spec.name          = 'react_rails_modal'
  spec.version       = ReactRailsModal::VERSION
  spec.summary       = 'Accessible modal dialog component on react-rails'
  spec.description   = 'React_rails_modal creates modal dialog easily on react-rails'
  spec.homepage      = 'https://github.com/kiyodori/react_rails_modal'
  spec.license       = 'MIT'

  spec.authors       = ['kiyodori']

  spec.add_development_dependency 'bundler', '>= 1.13'
  spec.add_development_dependency 'rspec', '>= 3.0'
  spec.add_development_dependency 'rails', '>= 3.2'
  spec.add_development_dependency 'react-rails', '>= 1.7.0'

  spec.files         = Dir[
    'lib/**/*',
    'README.md',
    'LICENSE.txt'
  ]

  spec.require_paths = ['lib']
end
```

今回はreact-railsとセットで動くGemなので、	`rails`と`react-rails`を`add_development_dependency`に追加します。
`add_development_dependency`に開発に必要なGemを記載することで、Gemfileはこの中身を参照します。

## Gemの本体の実装

それではGem本体を下記手順で実装していきます。

1. Asset Pipelineのパスにreact_rails_modalファイルが読み込まれるよう設定する
2. Asset Pipelineにreact_rails_modalファイルを追加するRailsコマンドを作成する。具体的には、マニフェストファイルに`//= require react_rails_modal`を追記するRailsコマンドを作成する
3. 設定したパスにreact_rails_modalファイルを設置する

Gem本体の実装は`lib/`ディレクトリに行います。

```
── lib
    ├── react_rails_modal
    │   └── railtie.rb
    │   └── version.rb
    └── react_rails_modal.rb
```

呼び出し側でGemが読み込まれると`lib/react_rails_modal.rb`が呼ばれるようになっているので、`lib/react_rails_modal.rb`で`lib/react_rails_modal/`ディレクトリ配下のファイルを`require`します。

```ruby:lib/react_rails_modal.rb
require 'react_rails_modal/version'
require 'react_rails_modal/railtie'
```

### 1. Asset Pipelineのパスにreact_rails_modalファイルが読み込まれるよう設定する

パスはRailsの起動時に設定されます。`Rails::Railtie`を使うことで、Railsの起動時にフックしてパスの追記を行います。

```ruby:lib/react_rails_modal/railtie.rb
require 'rails'

module ReactRailsModal
  class Railtie < ::Rails::Railtie
    GEM_ROOT = Pathname.new('../../../').expand_path(__FILE__)

    config.react_rails_modal = ActiveSupport::OrderedOptions.new

    initializer 'react_rails_modal.set_variant' do |app|
      app.config.assets.paths << GEM_ROOT.join('lib/assets/javascripts/').to_s if app.config.respond_to?(:assets)
    end
  end
end
```

`Rails::Railtie`を継承したクラスで、`initializer`マクロを用いて、Railsの起動プロセス時に`config.assets.paths`にパスを追記します。
これにより、`lib/assets/javascripts/`がAsset Pipelineのパスに追加されます。

実際に本Gemを読み込んだアプリケーションでパスを確認してみると、次のようになっています。

```
$ rails console

[1] pry(main)> Rails.application.config.assets.paths
=> ["{other_path}", ..., "{project_path}/vendor/bundle/ruby/{ruby_version}/gems/react_rails_modal-{gem_version}/lib/assets/javascripts/"]
```

最終的には`lib/assets/javascripts/`に`react_rails_modal.js`ファイルを置いておけば、マニフェストファイルでreact_rails_modalファイルが宣言された時に読み込まれます。

### 2. Asset Pipelineにreact_rails_modalファイルを追加するrailsコマンドを作成する

Asset Pipelineにreact_rails_modalファイルを追加するために、下記Railsコマンドを準備しました。

```
$ rails g react_rails_modal:install
```

上記コマンドにより、下記が`application.js`に追記されます。

```js:app/assets/javascripts/application.js
//= require react_rails_modal
```

このコマンドは、Railsジェネレータを利用することで作成できます。

`lib/generators/react_rails_modal/install_generator.rb`というファイルを作成します。

```ruby:lib/generators/react_rails_modal/install_generator.rb
module ReactRailsModal
  module Generators
    class InstallGenerator < ::Rails::Generators::Base
      desc 'Install react_rails_modal'

      def inject_react_rails_modal
        require_react_rails_modal = "//= require react_rails_modal\n"

        if manifest.exist?
          manifest_contents = File.read(manifest)

          if match = manifest_contents.match(/\/\/=\s+require_tree[^\n]*/)
            inject_into_file manifest, require_react_rails_modal, { before: match[0] }
          else
            append_file manifest, require_react_rails_modal
          end
        else
          create_file manifest, require_react_rails_modal
        end
      end

      private

      def manifest
        Pathname.new(destination_root).join('app/assets/javascripts', 'application.js')
      end
    end
  end
end
```

`Rails::Generators::Base`を継承したクラスを作成します。ジェネレータが起動されると、ジェネレータ内で定義されているパブリックメソッドが定義順に実行されます。

今回は`inject_react_rails_modal`メソッドの中で、`app/assets/javascripts/application.js`に`"//= require react_rails_modal\n"`を追記しています。

### 3. 設定したパスにreact_rails_modalファイルを設置する

ここまででAsset Pipelineに、パスを設定し、react_rails_modalファイルを追加できました。あとは設定したパスにreact_rails_modalファイルを置くだけです。

今回はreact_buildsディレクトリに、Reactでモーダルを実現するためのjsxファイルを置き、それをwebpackでjsファイルにコンパイルしています。
コンパイルされたjsファイルを、上記で設定したパスである`lib/assets/javascripts/`以下にreact_rails_modal.jsとして設置しています。
ここはJavaScript関連の処理になるので、具体的な内容は省略します。

このコンパイル作業をRakeタスク化し、下記コマンドで簡単に呼び出せるようにします。

```
$ rake react_rails_modal:build
```

Rakeタスクの定義はRakefileに行います。

```ruby:Rakefile
begin
  require 'bundler/setup'
rescue LoadError
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end

Bundler::GemHelper.install_tasks

namespace :react_rails_modal do
  desc 'Build the JS bundles with Webpack'
  task :build do
    Dir.chdir("react_builds") do
      `yarn run build`
    end
  end
end
```

Rakeタスクでは`yarn run build`コマンドを実行していて、その中でwebpackを用いて、Reactのjsxファイルをjsファイルにコンパイルしています。

このように、`lib/assets/javascripts/`以下にreact_rails_modal.jsを設置することで、Asset Pipelineから読み込むことができます。

以上で完成です。

## まとめ
JavaScriptライブラリをGemにする手順を見てきました。
まとめると、下記手順になります。

1. Gemのひな形を`bundle gem`コマンドで作成する
2. gemspecにGemの基本的な情報をまとめる
3. Gemの本体である`lib/react_rails_modal.rb`で必要なファイルを読み込む
4. Asset Pipelineのパスにreact_rails_modalファイルが読み込まれるよう設定する
	* パスの追加は、`Rails::Railtie`を使い、Railsの起動時にフックして設定する
5. Asset Pipelineにreact_rails_modalファイルを追加するrailsコマンドを作成する
	* マニフェストファイルに`//= require react_rails_modal`を追記するRailsコマンドを作成する
	* Railsコマンドは`Rails::Generators::Base`を用いることで設定できる
6. 設定したパスにreact_rails_modalファイルを設置する
	* Reactのjsxファイルをjsファイルにコンパイルしたものをパスに設置する

JavaScriptライブラリをGemにされる時に参考になれば幸いです。