仕事でgulpを使って僕がよくいじる一部のjsを高速でビルドできるようにしてみたのだが、JSをいじらない人の手元でなにかと差分がおかしくなったり、ビルドするタイミングを伝えないことで問題起きたり、その為に呼ばれて治すのがとにかくめんどくさかったため、思い切ってすべてのビルド工程をbrowserify_railsとbrowserify-incrementalと(そのtransform)に押し込んで、gulpを排除してみた。
そんで、browserify_rails は導入しただけだと嬉しさがあまりないので、スクリプトを書いて一気にcommonjsに置き換えることにした。
browserify_rails に関してはhokacchaさんの記事が便利。
モダンJavaScript開発環境 on Rails - クックパッド開発者ブログ
脇道にそれるが、僕は最初、これをbrowserifyを模した別の実装だと思ってスルーしていた。使ってみたら勘違いで、railsプロセスがbrowserify-incrementalを掴んで実行する実装で、それなりに便利だった
下準備
コミットログを見る限りこういうことをやったっぽい。
- js系のgem を除去(backbone-railsとか)
- browserify_rails の適用
- babel6 環境の構築
- browserify オプションを整える(既存のコードをparseするための coffeeifyとか)
- top level this の置き換え(this => window)
- top level var の置き換え(
var hoge
=>window.hoge = ...
) - sprockets require はコードより後にないといけないので、並び替える
- SprocketsのJSTをとりあえず
require_tree ./t
してしまう(本当は削除したい)
注意: 元々npm環境はあったので色々はしょってると思う
スクリプトを流す
babelで書いてた。browserify-rails のセットアップしていたら .babelrc はあると思うので割愛。preset-es2015があればよいはず。
// $ npm install glob
// $ npm install babel-cli -g
// # Move to your Rails root and put this script
// $ babel-node sprockets2commonjs.js
let glob = require('glob')
let fs = require('fs')
let path = require('path')
const ROOT = "app/assets/javascripts"
const coffeeExpr = /#= require (.*)/g;
const jsExpr = /\/\/= require (.*)/g;
const coffeeCommentOut = "#";
const jsCommentOut = "//";
/* Ignore rules */
const whitelist = [
"i18n"
];
/* Just comment out */
const commentOutList = [
/^t\//,
/^i18n\//
];
function convertSprocketPathToCommonjsPath(root, fpath, spath) {
if (/^\./.test(spath)) {
return spath;
}
let relToRoot = path.relative(fpath, root);
let rel = path.join(relToRoot, spath).replace(/^(\.\.\/)/, "");
return rel.indexOf("..") > -1 ? rel : "./" + rel;
}
function listIncludesPath (list, p) {
for (let w of list) {
if (
(typeof w === "string") &&
w === p
) {
return true;
} else if (
(w instanceof RegExp) &&
w.test(p)
) {
return true;
} else {
continue
}
}
return false
}
glob.sync(`${ROOT}/**/*`, {nodir: true}).forEach(f => {
let c = fs.readFileSync(f).toString();
const expr = (path.extname(f) === ".coffee") ? coffeeExpr : jsExpr
const commentOut = (path.extname(f) === ".coffee") ? coffeeCommentOut : jsCommentOut
let ret = c.replace(expr, (_, p) => {
if (listIncludesPath(whitelist, p)) {
return `${commentOut}= require ${p}`
}
if (listIncludesPath(commentOutList, p)) {
return `${commentOut} require('${p}');`
}
return `require("${convertSprocketPathToCommonjsPath(ROOT, f, p)}");`
});
if (ret != c) {
fs.writeFileSync(f, ret)
}
});
非常に雑なので参考までに。このスクリプトは全ての require を 相対パスに変換しながら commonjs に置き換える。coffeeとjsに対応。(今回はこれだけしか要らなかったので)。発想はシンプルなので、ぶっちゃけrubyなりで書いた方がいいと思う。
実際には、このスクリプトで問題の全てを解決はしないと思う。流したあとに微調整が必要になるだろう。適当にスクリプトを書き換えるなり、返還後のものを微調整する必要がある。
感想戦
最初は手作業でやっていたのだが、書き換えてる最中にミスが頻発したり、書いてる間にどんどんコンフリクトしていったので、諦めて変換スクリプトを書いたが、それが正解だったと思う。下拵え => 変換スクリプトでコンフリクト耐性があった。
とはいえ、もともと全てのjsを1つに圧縮してしまっていたため、比較的楽だったとは思う。