ネットショップ運営サービス カラーミーショップで「新カゴプロジェクト」と呼んでいる最高のショッピングカートの開発をしている@tsuchikazuです。
2014年に開発を開始した新カゴプロジェクトではフロントエンドをCoffeeScript + Angularで開発してきました。ES5までの時代にAltJS文化を作り、Class構文やArrow Functionを先取りしていたCoffeeScript。それらはES2015(ES6)の仕様に採用され、一方でCoffeeScript自体の開発は止まり、CoffeeScriptは役割を終えたのではないでしょうか。先月、今後も変化し続けるフロントエンドに追従するためにも、新カゴプロジェクトで200ファイル以上のCoffeeScriptをES2015へ移行しましたので、今回その方法を紹介します。
トランスパイラ
移行方法としてCoffeeScriptをES5へコンパイルしてからES2015に変換しようとすると、Classなど多くの情報がなくなってしまいます。そのためCoffeeScriptからES2015へ直接変換するトランスパイラを使います。トランスパイラとして有力なものは、decafとdecaffeinateの2つです。
decafは今すでに多くのCoffeeScriptのsyntaxに対応しているトランスパイラです。 CoffeeScriptコード -> CoffeeScript AST -> JS AST -> ES2015コード
という順で変換していき、CoffeeScript ASTを生成するparserにはcoffeescriptが利用されています。CoffeeScript ASTになったタイミングで、1行コメントやTemplate Stringsなど失われてしまう情報が存在していて、それらはES2015へ移行することができないのが弱点です。
decaffeinateは、decafよりも前に作られていたトランスパイラでdecafの弱点がカバーされています。変換の流れは一緒です。ただCoffeeScript ASTを生成するparserに違いがあり、v1まではCoffeeScriptReduxが利用されていました。しかし、CoffeeScriptRedux自体の不具合もあり全く使い物になりませんでした。(decafが生まれたのも、decaffeinateが使い物にならなかったからだったのです) v2から大きく変更が入りparserにはcoffeescriptが使われるようになりました。それに加え独自にCoffeeScriptコードのparseを行い、coffeescriptによるparseだけでは失われてしまう情報を補完するようになっています。それによって、1行コメントやTemplate Stringsの移行も可能にしています。ただ、最近開発が活発にされるようになったものの、まだ対応できていないCoffeeScript syntaxが多く存在するようです。
先月の移行時点では、decaffeinate v2の開発がまだあまり進んでおらず、decafの方が弱点はあるものの変換できるコードが多かったため、decafを採用しました。それぞれWeb上で動作を確認できるページも用意されていますので、まずはそちらで気軽に試して見てください。試すとわかりますが、どちらのツールもまだ完全に変換することはできません。なにか不具合を発見したらissueを登録してあげましょう。
移行手順
大きく以下の手順で移行しました。
- decafでの変換エラーを修正
- CoffeeScriptを修正
- 実装コード・テストコードを変換
- 後始末
decafでの変換エラーを修正
実際の移行作業の前段階として、CoffeeScriptでの開発を進めながらタスクの合間など時間があるときにdecafを試しに実行していました。最初は変換エラーがあったのでissueを登録したり、修正できるところはPRを送ったりしました。無事、全て変換できる状態にdecafをしてから次のステップへ進みました。
CoffeeScriptを修正
前ステップで変換はできるようになったものの、変換後のJSが複雑になる部分について事前にCoffeeScriptを修正しました。
具体的には hoge?.fuga()
このようなExistential Operator ?
と .
でchainするような部分です。今のプロジェクトではこれを多用していて、変換後のJSが非常に汚くなっていましたので、?.
を使用しないように修正しました。これを検出する簡単なcoffeelint ruleも公開しているのでよかったら使ってみてください。
# 修正前 CoffeeScript
hoge?.fuga?.piyo()
# 修正後 CoffeeScript (厳密には修正前と等価ではないので、注意)
hoge && hoge.fuga && hoge.fuga.piyo()
# 修正前 変換したJS
var ref;
typeof hoge !== "undefined" && hoge !== null ? ((ref = hoge.fuga) != null ? ref.piyo() : void 0) : void 0;
# 修正後 変換したJS
hoge && hoge.fuga && hoge.fuga.piyo();
実装コード・テストコードを変換
CoffeeScriptの修正が終わったら、トランスパイラで一気に変換します。 変換後のJSは、ES2015だけではなくStage 1のES Class Fields & Static Propertiesが使われたJSが出力されるため、babelでコンパイルする際にはClass properties transformのpluginが必要なので注意してください。
変換する際に気をつけた点は、実装コード・テストコードの順で変換し、変換前の「CoffeeScriptのテストコード」で、変換後の「JSの実装コード」をテストすることです。基本的には変換前のテストコードでテストが全て通れば、変換前と同じ品質を担保できていると考えました。今のプロジェクトではJasmineによるUnit TestとProtractorによるE2E Testを書いていて、実際に幾つか変換に伴う不具合があったのですが、それらは全て自動テストで発見することができました!念のため手動でのテストも行いましたが不具合はなく、リリース後も今のところ不具合が見つかっていません!!
後始末
decafはコメントを移行できないので、必要そうなコメントは泣きながら手動でコピペしました…不要なコメントは書かない方針で開発していたため、それほどコメントが書かれていないのが不幸中の幸いでした。
また、CoffeeScriptは最後の式は必ずreturnされることの影響で変換後のJSに不要なreturnが多かったため、テストコードだけでも不要なreturnを削除しました。
$ find test_dir -type f | xargs sed -i "" "s/return describe/describe/g"
$ find test_dir -type f | xargs sed -i "" "s/return it/it/g"
$ find test_dir -type f | xargs sed -i "" "s/return expect/expect/g"
以上でリリースをして、JSエラーを監視していましたが問題は発生しておらず無事移行を完了することができました。
まとめ
最初のステップの「decafでの変換エラーを修正」は別にすると、移行作業は2〜3日ほどで完了し、リリースすることができました。これぐらいのスピードでリリースできたのは、全てテストが書かれていたお陰だと思います。テストがなければ、今回のタイミングで移行に踏み切ることも難しかったでしょう。フロントエンドでもきちんとテストを書くことで、変化し続けるフロントエンドに対応していくことができるかもしれません。
また個人的には今回使ったdecafへの不具合報告から、CLIツール・Web上で動作確認できるTry itページ作成など色々Contributeができたのがよかったです。繰り返しになりますが、decafにしてもdecaffeinateにしてもまだまだ完璧ではありません。みんなで不具合を報告したりPRを送ってツールの完成度をあげ、ES2015へ移行していきましょう。