decaffeinate を使用して CoffeeScript を JavaScript に変換する

Tokyo Otaku Mode ではオタクコンテンツに特化した海外向け EC サイト otakumode.com を運営しています。

このサイトは Node.js と mongoDB、redis 等を使用して構築しています。2012 年から継続してサイト開発を行なっており技術的負債が散見される状態となっていたため、2018年1月を負債解消月間と設定し、通常の開発は一切行わず負債解消作業を行いました。

負債解消月間に先立ち開発チームで技術的負債をリストアップしたところ、

  • CoffeeScript の使用をやめ JavaScript にする
  • 古い npm module を一掃する
  • Vue 1.0 を 2.0 にバージョンアップする
  • アプリケーションサーバを docker コンテナで動かす

が効果が大きいのではないかという話になり実施しました。今回の記事では decaffeinate についてご説明したいと思います。

Table of contents

CoffeeScript to JavaScript overview

CoffeeScript は AltJS (alt JavaScript) の一つで、Python Like なインデントによるブロック構文を採用したプログラミング言語です。アロー関数、class / extends syntax によるクラス定義、デフォルトの変数スコープを関数ローカルに強制する等、ES5 以前の ECMAScript にあった多くの問題を解決した実装として2012年から2015年頃に人気がありました。

ES2015 (ES6) がリリースされ大半の機能が ECMAScript に導入されたことや、ES2015 に対応した CoffeeScript2 のリリースが少し遅れたため、一時期と比べ人気は下火になっていると個人的には認識しています。

Tokyo Otaku Mode では大半のコードを CoffeeScript1 で記述しており、CoffeeScript2 にバージョンを上げるか ES2016 に変えるかいずれかの対応を取る必要があったのですが、現在の JavaScript シーンが ES2016 を中心に動いている事などから ES2016 を選択しました。

なお、クライアントサイドの CoffeeScript については平行して行った Vue 2.0 へのバージョンアップ作業とコンフリクトするため CoffeeScript のままとして、サーバサイドコードをすべて JavaScript に置き換えることをゴールとして作業を行いました。

decaffeinate

CoffeeScript を JavaScript に変換する場合、decaffeinate というツールで自動変換することができます。

以下のようにファイルやディレクトリを引数に実行すると CoffeeScript が JavaScript に変換されます。

1
2
decaffeinate index.coffee
decaffeinate ./

https://github.com/decaffeinate/decaffeinate に書かれている通り decaffeinate にはいくつかのオプションがあるのですが、Node.js や Babel の使用を前提とする場合以下のオプションを指定しておいた方が良いと思います。

1
2
3
decaffeinate --loose-for-expressions --loose-for-of \
--loose-includes --disable-babel-constructor-workaround \
--disallow-invalid-constructors .

各オプションの意味は以下のようになっており、すべて指定すると Array.from への展開を抑制し、不正なコンストラクタがエラーになります。

  • –loose-for-expressions: expression loop に変換されるコードを Array.from で囲まない
  • –loose-for-of: for…of loop に変換されるコードを Array.from で囲まない
  • –loose-includes: includes に変換されるコードを Array.from で囲まない
  • –disable-babel-constructor-workaround: コンストラクタ内での super の前に評価される this に対して Babel/TypeScript workaround code を含ませないようにする
  • –disallow-invalid-constructors: コンストラクタ内で super の前に this が呼び出された場合や、サブクラスのコンストラクタ内での super の省略をエラーにする

decaffeinate suggestions

Decafffeinate は処理中で変換しきれないコードに対して以下のような decaffeinate suggestions をファイル先頭に出力します。

1
2
3
4
5
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/

この decaffeinate suggestions は、「CoffeeScript の暗黙的 return を明示的な return に変換したため不要な return が入っているかもしれないので削除した方が良い」という意味です。

decaffeinate suggestions の一覧は https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md にあります。

これらの decaffeinate suggestions のうち、DS0XX: Highest priority にリストされている項目は環境によっては修正しないとプログラムが動かなくなります。

現時点で DS0XX に挙げられている項目は DS001: Remove Babel/TypeScript constructor workaround のみです。DS001 は --disable-babel-constructor-workaround --disallow-invalid-constructors オプションを併用すると decaffeinate 実行時にエラーになりますので、エラーが発生しない状態になるよう先に CoffeeScript のコードを書き換えてしまう方が対応としては簡単と思います。

また DS1XX: Common cleanup tasks にリストされている項目については修正することが推奨されています。

DS1XX のうち DS101: Remove unnecessary use of Array.from については、デフォルトではかなり冗長なコードが出力されるため --loose-for-expressions --loose-for-of --loose-includes オプションを指定することで一部の Array.from への変換を抑制することができます。

その他の decaffeinate suggestions については修正しないと動かない訳ではないのでそのままでも良いですが、不要なコードもあるので可能なら修正した方が良いでしょう。ただし修正にはそこそこの時間が必要になると思いますので、修正する時間が充分取れない場合は放置した方が良いかもしれません。

弊社でこの作業を行った際、1950ファイルあった CoffeeScript を259ファイルに減らしました(上述の通りクライアントサイド CoffeeScriptはそのままにしたため一部ファイルが残っています)。当初は decaffeinate suggestions をすべて修正していたのですが、途中で明らかに終わらなさそうな状態になったため放置する方針に変更しています。

bulk-decaffeinate

大量に CoffeeScript がある場合、bulk-decaffeinate を使用すると一括変換できます。

bulk-decaffeinate で変換する場合、ソースが git で管理されている必要がありますので未だソース管理していない場合や他のツールで管理している場合は先に git リポジトリを作成してください。

bulk-decaffeinate は dry-run モードで decaffeinate を実行しエラーがあれば中断するようになっています。decaffeinate 成功後 git mv で CoffeeScript を JavaScript へ移動して commit するため git の commit ログが繋がった状態を保つことができます。また eslint --fix も合わせて実行するなど、decaffeinate に関わる一連のタスクを自動化できます。

なお、bulk-decaffeinate を使用する場合、decaffeinate のオプションは configuration ファイルの decaffeinateArgs に記述する必要があります。上述の decaffeinate オプションで実行する場合、以下の内容で bulk-decaffeinate.config.js を作成してください。

1
2
3
4
5
6
7
8
9
module.exports = {
decaffeinateArgs: [
'--loose-for-expressions',
'--loose-for-of',
'--loose-includes',
'--disable-babel-constructor-workaround',
'--disallow-invalid-constructors',
],
};

bulk-decaffeinate check でファイルチェック、

1
bulk-decaffeinate -c bulk-decaffeinate.config.js check

bulk-decaffeinate convert で JavaScript への変換を実行します。

1
bulk-decaffeinate -c bulk-decaffeinate.config.js convert

Git log and blame for renamed or deleted files

上述したように、bulk-decaffeinate を使用して decaffeinate した場合 git mv で .coffee ファイルを .js ファイルに変換して git commit します。git mvgit rm した後で、削除されたファイルを引数指定して git log しても表示されません。decaffeinate してから古いファイルのログを見ようとするとオプションを工夫する必要があります。

log

a.coffee を a.js に git mv してコミットした場合、--follow オプションを付けて a.js のログを表示すると a.coffee のログも表示されます。

1
git log --follow a.js

a.coffee のログだけを見たい場合は、--all オプションを付けてログを表示してください。(bulk-decaffeinate を使わない場合等) git rm してから git add した場合も --all で削除したファイルのログが見れます。

1
git log --all -- a.coffee

(余談ですが -- はオプションの終わりを示す引数です。getopt(3) で定義されていますので、オプションを解釈する一般的な *NIX プログラムで使用できます。)

blame

リネーム/削除問わず、HEAD 上に存在しないファイルの git blame を見る場合は、対象コミットの一つ前のコミットを指定する必要があります。

1
2
3
git blame \
`git log -2 --pretty=format:"%h" -- a.coffee | tail -1` \
-- a.coffee

After decaffeinate

decaffeinate した後のソースコードが特に読みにくいという訳でもないと思いますが、prettier 等でコードフォーマットしておくと以降の開発が楽になると思います。

クライアントサイド JavaScript の場合は BabelBabel Transform Plugin で実行を想定するブラウザのバージョンに合わせてトランスパイルするようビルドツールを設定した方が良いでしょう。

CI を稼働させている場合 eslint を CI で実行し、lint エラー時に CI を失敗するように設定しておくとコードチェックが自動化し対応を強制できます。decaffeinate を実行してすぐの状態ですと eslint がエラーを出すと思いますので若干手間が必要ですが、prettier と eslint –fix の併用で自動変換してから問題のありそうなエラーを修正した後、コードレベル設定で一部 lint を無効にしておく形でしたら割と簡単に実装できると思います。(eslint の設定方法は https://eslint.org/docs/user-guide/configuring にあります。)

Recruitment

Tokyo Otaku Mode では以下の職種を募集しております。Node.js、mongoDB、Vue.js でのシステム開発にご興味のある方は是非ご連絡ください。