Gruntfileを整理してタスクの自動化を進めた

(急遽宣伝) FrontrendでGruntします

Frontrend(フロントレンド)とは サイバーエージェントが主催するフロントエンド系技術セミナーです。 HTML5/CSS3やJavaScriptのトレンドやノウハウ等を惜しみなくお伝えします。

2012/10/21(日)の午後に、FrontrendでGruntのことを紹介させていただく運びになりました!以下の記事は色々と知ってる前提な内容になってしまっているので、ベーシックな所から知りたい方には特にオススメです。

今回のFrontrendはスピード特集ということで、ページパフォーマンスの最適化はもちろん、CSSプリプロセッサの導入や、ガイドラインの運用による業務効率のカイゼンにも踏み込んでスピードを上げていきます!ということで、東京のスピード感だとあっという間に埋まってしまいそうなので、お申し込みはお早めに〜

宣伝ここまで

grunt-sandboxで捗ってきた

自分用のGruntコレクションを、引き続きGitHubで管理しています。エディタやサーバーサイドに依存しないので、持ち運びカンタン。

簡単な導入方法はInstallationに書きましたが、詳しい使い方はもうちょい整理してからということで。んで、以下のようなtaskを入れて、色々とカバーしてみましたので、今回はそれを紹介しつつの備忘録。結構苦労しました…。

Grunt 0.4.0

Gruntは現在のメジャーバージョンだと0.3.14ですが、開発ブランチでは0.4.0が控えています。例えば、自分が把握できた限りでは、以下のような変更があります。(網羅はまったくしてないです)

  • 定義ファイル名が、grunt.js が Gruntfile.js にリネームされました
  • 定義ファイルが.coffeeであっても実行できるようになりました
  • grunt.utilsgrunt.utilにリネームされました
  • this.filethis.filesにリネームされました(in task context)
  • grunt.file.watchFilesが加わりました (in grunt-contrib-watch)
  • <config:taskName.dist.prop>でなく<%= taskName:dist.prop %>
  • <json:package.json>でなくgrunt.file.readJSON('package.json')
  • タスクの複数指定がスペース区切りから、配列になりました(grunt.regsiterTask、watch.tasksなど)
  • grunt.registerHelperが廃止されました(Helpers and directives? Gone.

どーも、メンテされていない作りっ放しなgruntタスクがnpmに散らばってることを踏まえると、これらの変更が影響を与える可能性も否めない。さっさと0.4.0になってもらえると、周りに説明するとき諸々の都合がよいのですがー。

変更点の中でも、grunt.file.watchFilesが特に強力なので、自分は最初から0.4.0の開発版を利用しています。そのため、以下もすべて0.4.0の開発版前提ということで悪しからず。forkして微調整した各taskも同様です。

2012/10/20追記:grunt.file.watchFilesは、grunt-contrib-watchの機能として内包され、grunt.fileのAPI docsからは削除されています。最新情報は、各自参照してください。:)

grunt-contrib(便利パック)

あらためての紹介ですが、色々入っていて便利なgrunt-contribです。

  • bump - Bump package version.
  • clean - Clear files and folders.
  • coffee - Compile CoffeeScript files into JavaScript.
  • compress - Compress files and folders using gzip or zip.
  • copy - Copy files into another directory.
  • handlebars - Compile handlebars templates to JST file.
  • jade - Compile Jade templates to HTML.
  • jst - Compile underscore templates to JST file.
  • less - Compile LESS files to CSS.
  • mincss - Minify CSS files.
  • requirejs - Optimize RequireJS projects using r.js.
  • stylus - Compile Stylus files into CSS. Preloaded with nib.
  • yuidoc - Compile YUIDoc Documentation.

今回の構成ではとりあえずyuidocのみ利用しています。

最近、grunt-contribというひとつのパッケージから、grunt-contrib-xyzのような独立したパッケージに分割されました。現在のgrunt-contribは、package.jsonのdependenciesに、個々にパッケージが書かれているだけになっています。これらは、0.4.0に対応した修正も加わっているので、develブランチのgruntでも問題なく利用できます(たぶん)

yuidoc(ドキュメント生成)

ドキュメント生成もGruntのタスクに入れることにしました。以前PHPでphpDocumentorを利用していたことがありましたが、JavaScriptのドキュメント生成はなにげに初ということで、これを機に導入します。

今回はgrunt-contribの組み込みタスクにあるYUIDoc - Javascript Documentation Toolを利用します。そのままオプションを渡して特に問題なく使えるのでOK。

特に良かったのがCoffeeScriptのコメントも解析してドキュメント生成できること。プロジェクトによって、JavaScriptを書くのかCoffeeScriptを書くのか不定なところもあり、どちらからでもドキュメント生成できるようにしたかったので、ちょうどよかったです。

grunt.initConfig({
  yuidoc: {
    dist: {
      'name': 'Project Name',
      'description': 'Project Description',
      'version': '0.0.2',
      'url': 'http://projecturl.com/',
      options: {
        paths: 'src/coffee',
        // paths: 'dest/js',
        outdir: 'docs',
        syntaxtype: 'coffee',
        extension: '.coffee'
      }
    }
  },
});

これ、わりと最近のpull requestで実装されたみたいで、危うく気づかずに回りくどいアプローチをする羽目になるところでした。.coffeeのコメントはこんな感じで書きます。

grunt-img(画像の最適化)

画像の最適化をしてくれます。いわゆるロスレス圧縮的な。optipngとjpegtran使えばよいかな〜、と考えていた矢先、npmで検索したら出てきてくれたので、そのまま利用しています。

% brew install optipng jpeg

利用するには、brewなどでoptipngとjpegtranをインストールしておく必要があります。

grunt.initConfig({
  img: {
    dist: {
      src: ['src/img/**/*.png', 'src/img/**/*.jpg', 'src/img/**/*.jpeg'],
      dest: 'dist/img'
    }
  },
  optipng: {
    args: ['-o5']
  }
  jpegtran: {
    rescan: "./jpegrescan"
  }
});

srcとdestを指定すればok。あと、OptiPNGがそのままだとImageOptimと比べて圧縮率低めなので、-o5というオプションを追加しておきます。-o7が最大で、さらに-zm1-9を追加すると効果を高められるようですがすごく時間かかります。

Jpeg画像についても、ImageOptimのほうが圧縮率が若干高いのですが、これはPerl Dark Shikari - Pastebin.comjpegrescanというperlスクリプトを利用しているためです。ということで、forkした自分用のgrunt-imgには、jpegrescanへのパスを指定できるオプションを加えてあります。ここまでやると、ImageOptimと同等の圧縮率になりました。

grunt-jasmine-task(Headlessなユニットテスト)

前回の続Grunt + phantomjs + jasmineで自動テスト環境では、execを通して自力でphantomjsとgrowlにつなげていましたが、今回は既製のpackageを利用します。phantomjs用の実行スクリプトも同梱されているので、これを入れるだけでJasmineを使ったHeadlessテストが利用できます。

QUnitだったらそもそもGrunt本体の組み込みタスクなんですけどね

今回もwatchタスク用で、変更のあったファイル検知して「単体のアプリケーションコードと、そのテストコード」のみを含むSpecRunner.htmlを自動生成しています。これによって、変更中のファイルの単体テストが、変更を保存するたびに逐次確認できるようになります。すべてのテストを、ひとつファイルを保存するたびに走らせるわけにもいかないので。

// Project configuration.
grunt.initConfig({
  // Headless test with jasmine
  'build-runner': {
    part: {
      pairing: {
        'dist/js' : 'test/spec'
      },
      watch: ['dist/js/**/*.js', 'test/spec/**/*.js']
    }
  },
  jasmine: {
    part: {
      src: ['test/SpecRunner.html'],
      errorReporting: true
    },
    all: {
      src: ['test/AllRunner.html'],
      errorReporting: true
    }
  },
  // Watch
  watch: {
    jasmine: {
      files: ['<config:build-runner.part.watch>'],
      tasks: ['build-runner:part', 'jasmine:part']
    }
  }
});

build-runnerというのがSpecRunner.htmlの生成taskです。ファイル更新の検知をしたら、先にbuild-runnerを動かしてSpecRunner.htmlを生成し、その後にjasmineによるテストをgrunt-jasmine-taskで走らせます。

SpecRunner.htmlの生成については、grunt-sandboxのリポジトリに入れてありますが、手っ取り早く見ると更新したファイル+対応するテストケースのみ含むheadlessテスト用のSpecRunner.html作るくん — Gistな感じです。

growlで表示するのは、思ったよりウザかったので今回はやめてしまっています

余談 jasmine-headless-webkit について

SpecRunner.htmlを自動生成してくれたり、Report形式えらべたりと、なかなか至れりつくせりに見えたjasmine-headless-webkitですが、今回採用しているrequire.jsとの相性が悪くて断念しました。相性が悪いというか、特に手段が用意されていないというか。

It won't work nicely, and I don't have time to make it work nicely right now. I'd suggest trying a different JS testing project for testing RequireJS code. does not play nicely with requirejs · Issue #136 · johnbintz/jasmine-headless-webkit

ということで、他の使った方がいいよ!っていう話らしいのでやめました。sprocketsとの相性は考慮されているようなので、require.jsでなくsnocketsのほうなら、問題なく使えるんじゃないかな〜、と思います。

grunt-stylus(.stylのコンパイル・fork)

コードを見る限り、grunt-contribの中にあるのと大差ないですが、こっちのリポジトリのほうが若干進んでいるようです。

で、本来ならnib(SassでいうCompassみたいなmixin集)を使えるはずなんですが、nibをstylus.use(require('nib'))するタイミングが悪く、オプションでpathsを指定するとnibのパスが上書きされてしまいます。ので、オプションのpathsが反映されたあとに、nibをrequireするよう修正しました。

2012/10/20追記:grunt-stylusでなく、grunt-contirb-stlusのほうに Devel fix some problems by ahomu · Pull Request #3 · gruntjs/grunt-contrib-stylus を送った。

+ ahomu/grunt-stylus

forkした修正版がこれです。まじめに管理する気はあまりないので、pull requestしてちゃっちゃと修正してもらいたいところ。

grunt.initConfig({
  // Stylus
  stylus: {
    dist: {
      files: {
        'dist/css/test.css' : 'src/stylus/test.styl'
      },
      options: {
        'include css': true,
        compress: true,
        urlfunc: 'embedurl',
        paths: ['src/stylus']
      },
      watch: ['src/stylus/**/*.styl']
    }
  },
  watch: {
    stylus: {
      files: ['<config:stylus.dist.watch>'],
      tasks: 'stylus'
    }
  }
});

ちなみにurlfunc: 'embedurl'というのは、Data URIに変換する関数の名前指定です。これを指定しておくことで、.stylファイルのbackground-image: embedurl(path/to/image.png)が、コンパイル時にData URIへ変換されるようになります。画像にうまくパスが通らないときは、pathsになんぞ追加すればよいでしょう。

grunt-coffee(.coffeeのコンパイル・fork)

+ avalade/grunt-coffee

2012/10/23追記:grunt-contrib-coffeeが良くなっていたのでそっちに差し替え

grunt-contribのがイマイチだったので、これも別途で入れています。こっちはstylusと違って、grunt-contribに入ってるものとは、全然別物のようです。ちゃんとメンテナンスが続いているようで好感もてる。

2012/10/20追記:forkしたリポジトリを廃止しました :P

grunt-coffeeについては、バグ方面では全く問題なかったのですが、watch実行すると.coffeeを保存するたびに同じ指定で含まれているファイルをすべてコンパイルしてしまって非効率でした。ので、ちょっと変更して、grunt.file.watchFilesに対応させています。

+ ahomu/grunt-coffee

こっちがforkして上記の変更をかけたほうのリポジトリです。

grunt.initConfig({
  // CoffeeScript
  coffee: {
    dist: {
      files: {
        'dist/js/*.js': ['src/coffee/**/*.coffee']
      }
    },
    test: {
      files: {
        'test/spec/*.js': ['test/spec/coffee/**/*.coffee']
      }
    }
  },
  // Watch
  watch: {
    coffeedist: {
      files: ['<config:coffee.files>'],
      tasks: 'clint:dist coffee:dist'
    },
    coffeetest: {
      files: ['<config:coffee.files>'],
      tasks: 'coffee:test'
    }
});

grunt-requirejs(依存管理・結合・圧縮)

RequireJSを利用した依存管理に、grunt-requirejsを利用します。grunt-contribにもrequre.jsのtaskがあったのですが、それはjrburke/almondの統合に対応してないようだったのでスルーしました。

で、やりたい事としてはdevelopment環境では基本的にrequire.jsをAMDらしく使って複数ファイルを管理し、production環境ではalmondを使って依存解決されたファイル結合済みの単体ファイルを利用したいという感じ。開発中はデバックの都合なども含めてrequire.jsでバラ管理したいのですが、リリース時にはHTTPリクエストなどの都合、ある程度の単位でまとめてしまいたい次第。

2012/10/20追記:本家が直っていて、0.4.0aにも対応していたのでforkリポジトリ消しました。pull requestしてなくてごめんね...

+ ahomu/grunt-requirejs at fix_almond

例によってforkしたリポジトリはこちらなわけですが、今回のalmondを利用した運用のためにはこの修正済みのリポジトリを使わないとだめです。色々とFix almond option & out option · 56c26f8 · ahomu/grunt-requirejsのようなことがありまして、細かくは端折りますがそのままだとalmondの利用に問題あります。

// Project configuration.
grunt.initConfig({
  requirejs: {
    dist: {
      almond: true,
      // modules: [
      //   {name: 'main'},
      //   {name: 'sub'}
      // ],
      // dir: 'almond',
      // appDir: 'dist',
      baseUrl: 'dist/js',
      include: ['main', 'sub'],
      paths: {
          // underscore: '../vendor/underscore',
          // jquery    : '../vendor/jquery',
          // backbone  : '../vendor/backbone'
      },
      pragmas: {
          doExclude: true
      },
      skipModuleInsertion: false,
      optimizeAllPluginResources: true,
      findNestedDependencies: true,
      out: 'dist/js/all-min.js'
    }
  },
});

この設定はoutオプション(依存関係を解決して、1ファイルにまとめてファイル出力する)を使うためのものです。この設定だと、dist/js/main.jsdist/js/sub.jsの両方を依存関係解決した上で、1ファイルにまとめて出力します。その他、色々とコメントアウトしていますが、これらをいじくるとファイル出力を調整できます。

たとえば、modules, dir, appDirをコメント外して、includeをコメントして、almondディレクトリにbaseUrlをjsにすれば、main.jsとsub.jsを個々に依存関係を解決した別個ファイルとして出力できます。

完全に1ファイルで完結させて結合するか、個々の依存関係ルートごとにファイルを生成するか、状況・環境に合わせてうまいことやりくりすれば良いのではないでしょうか。

lint clint(Lintの実行)

JavaScriptとCoffeeScriptについてですが、Lintをできるようにしています。普通のlintは、Grunt本体に組み込まれているタスクを利用していて、.coffee向けにはCoffeeLintを利用して、clint taskを書き起こしています。

CoffeeLint用に書き起こしたtaskは、これもリポジトリに入っていますが、coffeelint task — Gistな感じで適当にやってます。また、全部のconfigを引用するとlint周りは長いので、詳しく見たい方はリポジトリに入ってるコードを参考にしてください。

grunt.initConfig({
  // CoffeeScript
  coffee: {
    dist: {
      src: ['src/coffee/**/*.coffee'],
      dest: 'dist/js'
    },
    test: {
      src: ['test/spec/coffee/**/*.coffee'],
      dest: 'test/spec'
    }
  },
  clint: {
    dist: {
      files: ['<config:coffee.dist.src>']
    },
    test: {
      files: ['<config:coffee.test.src>']
    }
  }
});

ビルド時は、lintを通過できなかったら中断するようにしていますが、開発中に最後の最後でlintに引っかかるとストレスフルも甚だしいと思います。そもそも、リアルタイムにlintしてくれる賢いエディタを利用して、確実にlintを通過するコードを書ける環境を整えるべきでしょう。Sublime Text 2のLinterを入れても良いですし、Php/WebStormもデフォルトで賢く指摘してくれます。

あとlint系だと、Google Closure Linterとかもありますね。そんなに厳しい規則ではありませんが、守っておくとコードがきれいになるので、多人数で開発する際の品質管理には使えるかもしれません。Sublime Text 2のpackage(fbzhong/sublime-closure-linter)が中々良い感じでした。

オマケ: ドキュメント生成で yuidocに至るまでの紆余曲折

yuidocに至る前に、実は以下のようなライブラリも試していたりしました。いやー、最終的にjsもcoffeeもgrunt-contirbの組み込みタスクだけで良かったというオチにガックリ…orz

JsDoc Toolkit

デフォルトのドキュメント生成スタイルがダサい!と見かけましたけど、JasmineのReferenceがまんまデフォルトなんですね。自動生成されるReferenceとしてなら、個人的に十分な感じ。

その他、gigafied/node-jsdoc-toolkitという、JsDoc Toolkitのコア処理をnodeで実行しよう、みたいなライブラリもありましたが、今回は素直にbrew jsdoc-toolkitでインストールできるものを利用してました。

Rhinoで動いているものをnodeに移植してるだけなら、安定してそうではありますが。ところでJavaのJavaScript実装って、インドのインドネシア料理みたいですね。

Dox

DailyJS: Let's Make a Framework: JSDocで紹介されていたのですが、JsDocな記法をサポートするDoxというライブラリもありました。どうもコイツは、パースした結果をJSONとして吐いてくれるようで、それを好きなように料理すればOKというノリ。今回はさっさとHTMLまで落とし込みたかったので、JsDoc Toolkitでしたが解析結果を元にプログラム的な処理をゴリゴリかけたいときは有効そうなので、ナニカの時に使ってみたい。

CoffeeDoc

最後にCoffee Scriptのままコメントを解析してドキュメント生成してくれるパッケージ。@param みたいな記法に対応するわけではなさそうだけど、markdownに対応している。コメント内にmarkdownで表現していく感じで、デフォルトの出力htmlもまあまあ見やすく、導入もラクチン。JsDoc風の記法を覚えるコストがない分、ある意味お手軽。

しばらく続きそう

今の時点ではこのような構成ですが、テンプレートのコンパイルとData URIの生成について、まだ決定的な解決法が出来ていないので、もう少し進化する予定です。デプロイとか、統合テストとか、そのあたりのwatch系以外のCI的なタスクも増やしていきたい所。

Grunt自体が開発早いのもありますが、自分自身が業務ではまた違うGruntfileを使っているため、それのフィードバックなんかも取り込んでいけたらなと思っている次第です。