Edited at

app/assets/javascripts以下のJSを全てcommonjsのrequireに書き換える

More than 3 years have passed since last update.

仕事で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つに圧縮してしまっていたため、比較的楽だったとは思う。