CoffeeScript v2 on Rails

Ruby on Railsと言えばCoffeeScript、CoffeeScriptと言えばRuby on Railsと言われたぐらい、Railsでは昔からCoffeeScriptを採用していました。Ruby使いにとってはCoffeeScriptの文法は馴染みやすいものであり、また、モジュールなども無かった当初はSprocketsという考えも悪くありませんでした。

時代は移り、sprocketsだけでは対応しきれなくなったRailsはyarnやwebpackにも対応し、多様なJavaScriptエコシステムにも対応しようとしています。そんな中、CoffeeScriptも2系となって、ES2015+に対応した今、Railsにおいて、2系のCoffeeScriptをどうやって使うかを見ていきます。

Ruby 2.4.2p198
Ruby on Rails 5.1.4

## Railsアプリのセットアップ

まずは、普通にRailsのアプリを作ります。Ruby、Node.js、Yarn、Ruby on Railsは既にインストール済みで有ることの前提です。ただし、webpackをReactありで有効にします。

```
rails new coffee_app --webpack=react
```

```
rails g controller home index
```

config/routes.rbを次の用にしておきます。

```config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end
```

あとは、`rails s`で立ち上げ、アクセスしたときに、home/indexが表示されれば準備は完了です。今回はCoffeeScriptの動作だけを見ていきますので、特にモデルを作ったりはしません。おもに、この作ったページでどうなるかを見ていきます。

## Assets Pipeline

さて、普通にセットアップしたらapp/assets/javascript/home.coffeeが作られているはずです。Assets Pipelineによってこのhome.coffeeはJavaScriptにコンパイルされて読み込まれていることでしょう。では、このCoffeeScriptがv2に対応しているかというと、v1が使われてしまっています。

これは、一緒にインストールされているcoffee-script-sourceが内包しているCoffeeScriptがv1になっているからです。この部分を変えない限り、v2を使うことはできません。

### CoffeeScriptを指定する。

まずはコンパイル用のCoffeeScript v2を手に入れなくてはなりません。しかも、一つのファイルとしてまとまっている物でないと使用できません。そこで、[`"text/coffeescript"` Script Tags - CoffeeScript](http://coffeescript.org/#scripts)にあるブラウザ用のcoffeescript.jsを使用します。ダウンロードしてvendorディレクトリに入れておきます。

```
cd vendor
wget http://coffeescript.org/v2/browser-compiler/coffeescript.js
```

このファイルを見に行くようにするため、config/initializers/coffeescript.rbを下記のように作ります。

```config/initializers/coffeescript.rb
CoffeeScript::Source.path = ENV['COFFEESCRIPT_SOURCE_PATH'] ||
  Rails.root.join('vendor', 'coffeescript.js').to_s
```

これでCoffeeScript v2が使われるようになります。早速、hello.coffeeを次のように書き換えて、コンパイル後のJavaScriptファイルを見てみます。

```app/assets/javascript/home.coffee
class Test
  constructor: ->
    console.log 'create Test'

  hello: (s) ->
    console.log "hello, #{s}!"

test = new Test
test.hello('world')
```

ブラウザから、JavaScriptのソースを見ると次のようになっています。

```hello.js
(function() {
  var Test, test;

  Test = class Test {
    constructor() {
      console.log('create Test');
    }

    hello(s) {
      return console.log(`hello, ${s}!`);
    }

  };

  test = new Test;

  test.hello('world');

}).call(this);
```

ちゃんと、class構文を使っていますね。CoffeeScript v2化がうまくいきました。

### トランスパイルはできない。

さて、この方法には重大な欠点があります。それは続けてBabelによるトランスパイルができないと言うことです。そのため、次のことが使えません。

1. ES2015+に未対応のレガシーブラウザ(IE11等)では動作しないJavaScriptになってしまう。
2. `import`や`export`はさらにモジュール対応のブラウザが必要であり、フィンガープリントを無効にする必要がある。
3. JSXもJSXのままであるため、そのままでは動作しない。

Reactを使用せず、ブラウザが限られた内部向けのアプリでも無い限り使えるとは言えません。ここら辺がアセットパイプラインの限界と言ったところでしょう。

## Webpack

最新のRailsはwebpackerを使ってwebpackと連携して使うことができます。`--webpack`を指定してセットアップしたことで、既に使えるようになっているはずです。

まずは、home.coffeeをapp/javascript/packs/home.coffeeに移動しましょう。つぎに、app/views/home/index.html.erbで次のような行を追加します。

```app/views/home/index.html.erb
<%= javascript_pack_tag 'home' %>
```

うまくwebpackでCoffeeScriptが変換(pack)されて読み込みできるようになっているはずです。しかし、Rails 5.1.4ではこのとき使われるCoffeeScript v1です。同じく、v2に挙げなくてはなりません。

### YarnでCoffeeScriptをアップデートする。

RailsではJavaScriptのパッケージをYarnで管理できるようになっています。次のコマンドでCoffeeScriptをアップデートできます。

```
yarn add coffeescript
```

package.jsonでcoffeescriptだけがアップデートされたことを確認できるはずです。`rails webpacker:compile`でコンパイルして、作られたJavaScriptを確認してみましょう。

```home.js
/******/ (function(modules) { // webpackBootstrap
// webpack初期処理
/***/ 27:
/*!******************************************!*\
  !*** ./app/javascript/packs/home.coffee ***!
  \******************************************/
/*! dynamic exports provided */
/*! all exports used */
/***/ (function(module, exports) {

eval("var Test, test;\n\nTest = class Test {\n  constructor() {\n    console.log('create Test');\n  }\n\n  hello(s) {\n    return console.log(`hello, ${s}!`);\n  }\n\n};\n\ntest = new Test;\n\ntest.hello('world');\n//# sourceURL=[module]\n//# sourceMappingURL=...\n//# sourceURL=webpack-internal:///27\n");

/***/ })

/******/ });
```

classはclass構文のママですので、うまくCoffeeScript v2でコンパイルされています。

さて、このままではアセットパイプラインと同じ問題が起きてしまいます。なんとかしてBabelによるトランスパイルを実行させなくてはなりません。

### トランスパイル付きでCoffeeScriptを使う。

トランスパイル付きに対応したcoffee-loaderは0.9.0以上です。0.8.0が入ってしまっていますので、まずはcoffee-loaderをアップデートします。

```
yarn add coffee-loader
```

webpackのオプションをマージするためにwebpack-mergeを入れておきます。

```
yarn add webpack-merge
```

webpackerの設定はcofing/webpacker.ymlとconfig/webpack配下の各ファイルです。config/webpack/environment.jsを次のように書き換えます。

```config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const merge = require('webpack-merge')

const transpileCoffeeLoaderOptions = {
  transpile: {
    presets: ['env']
  }
}

const CoffeeLoader = environment.loaders.get('coffee')

CoffeeLoader.options = merge(CoffeeLoader.options, transpileCoffeeLoaderOptions)

module.exports = environment
```

JavaScriptのファイルを見るとclass構文では無く、ES5の文法で書いていることが確認できると思います。

参考: https://github.com/rails/webpacker/blob/master/docs/webpack.md

### さらにReactを利用する。

envプリセットだけではReactのJSX記法に対応しません。しかし、`--webpack=react`でセットアップしている場合は既にReactの環境は整っており、.babelrcが用意されています。この.babelrcを見に行くように書き換えます。

```config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const merge = require('webpack-merge')

const transpileCoffeeLoaderOptions = {
  transpile: {
    extends: `${__dirname}/../../.babelrc`
  }
}

const CoffeeLoader = environment.loaders.get('coffee')

CoffeeLoader.options = merge(CoffeeLoader.options, transpileCoffeeLoaderOptions)

module.exports = environment
```

それぞれ次のように書き換えます。

```app/javascript/packs/home.coffee
import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component
  render: ->
    <p>
      CoffeeScript + React on Rails!
    </p>

ReactDOM.render <Main />, document.getElementById('main')
```

```app/views/home/index.html.erb
<div id="main"></div>
<%= javascript_pack_tag 'home' %>
```

さっそく`rails s`で立ち上げて、確認してみます。


うまくいきました。

## まとめ

CoffeeScript v2がリリースされてから、周りのツールも対応が進んでいます。デフォルトではv1が使われる事がまだまだ多いと思いますが、少しバージョンアップすればv2の利用も十分可能です。

ただ、見て頂いたとおり、現実的にはwebpackでないとうまくいかないようです。ただ、Railsがwebpackをうまく取り込んでいるため、Railsでの作成が時代遅れになると言うことは無いと思います。CoffeeScriptもその中でいつまでも使えるようになるのでは無いのでしょうか。