JavaScriptをビルドするJavaScript

たまには素でJavaScriptファイルをビルドすることも考えないと頭がダメになりそう。他にgulpとかでかすぎるし、npm run-scriptだけでいけるいけるみたいな話を先行者以外からも聞くようになったので、そっちに比重を移すことも視野に入れたいとか。僕はビルド・ツールをnpm run-scriptで薄くラップする手法というのが現実的だと考えてて、それを確認したいというのもあった。

依存はnpmやBowerで解決している前提で、自前で書いたものを最小化し、依存と連結するようなものを想定しておく。つまり、

build/js/main.min.jsへとビルドするくらいにしておく。

#!/usr/bin/env node

'use strict';

var async = require('async');
var fs = require('fs-extra');
var path = require('path');
var uglifyjs = require('uglify-js');

async.waterfall([
  function (next) {
    fs.remove('tmp/', function (err) {
      next(err);
    });
  },

  function (next) {
    fs.remove('build/', function (err) {
      next(err);
    });
  },

  function (next) {
    fs.ensureDir('tmp/', function (err) {
      next(err);
    });
  },

  function (next) {
    fs.ensureDir('tmp/js/', function (err) {
      next(err);
    });
  },

  function (next) {
    fs.readFile('src/js/bar.js', 'utf8', function (err, data) {
      next(err, data);
    });
  },

  function (js, next) {
    fs.readFile('src/js/baz.js', 'utf8', function (err, data) {
      next(err, js + ';' + data);
    });
  },

  function (js, next) {
    try {
      next(null, uglifyjs.minify(js, {
        fromString: true
      }).code);
    } catch (err) {
      next(err);
    }
  },

  function (js, next) {
    fs.readFile(
      path.join(
        __dirname,
        'node_modules',
        'foo',
        'dist',
        'foo.min.js'
      ),
      'utf8',
      function (err, data) {
        next(err, data + ';' + js);
      }
    );
  },

  function (js, next) {
    fs.writeFile('tmp/js/main.min.js', js, function (err) {
      next(err);
    });
  },

  function (next) {
    fs.move('tmp/', 'build/', function (err) {
      next(err);
    });
  }
],

function (err) {
  if (err) {
    throw err;
  }
});

コード自体は簡単でわかりやすい。手軽なのでasyn.waterfall()でフローを制御。Streamじゃないけど、中間ファイルを吐かないのでGruntよりは速い。テキトーに書かれたモジュールもtry...catchしつつ流せるとか気軽に書ける。ファイルの保証やディレクトリの削除などはfs-extraパッケージに頼ればこんなもので済んだ。

……でも長い。これでもfs-extraパッケージのおかげで短くなってる。読み込みと連結をまとめたりとかでもっと短くできるけど、そこを抽象化するとストレートに書けるという長所が無くなっちゃう。asyncの代わりにPromisifyするともうちょっと楽だけど、それほど劇的でもない。

ビルド・ツールのキモはやはりグロブを使った抽象化を提供してくれるあたりにあり、そこを毎回自前でどうにかするのは面倒すぎる。Gruntやgulpのような巨大な依存を減らせることは確かだけど、その代わりに小さな依存が増えるので、あんまり変わらない。こういうのを書くためのパッケージをセットにしたメタ・パッケージみたいなのがあればまた少し話が変わる……かなー?

こういうのを抽象化して引数で入力と出力を指定して終わりというパッケージがあっても面白そうだけど、それだとGruntとあんまり変わらないっぽい。Gruntfile.jsを書く代わりにpackage.jsonで複雑な引数を書くわけだし。


つまりmakeはありだけど、npm run-scriptはなしかなーという感じになった。引数取れるようになったのでnpm run-scriptでいけるいけるとか言ってる人の話はじっくりと聞いて、本気でそれだけで済むと言っていたら聞き流した方が良さそう。