やってみたかったシリーズです。そろそろNode周りの環境をちゃんとしたかったので。 ちょっと長めのエントリになります。
2013/08/28:grunt.loadTaskの挙動について追記しました(;´Д`)
gruntの自分タスクをちゃんと作ってみる
今までは、grunt-contrib-xxxみたいなのをloadNpmTasksして使ってるだけだったんですが、そろそろgrunt内部の動きとか、オリジナルのタスク(これから必要になりそう)の作り方なんかも知っておきたかったので作ってみました。できたものはこちら。
npmには登録してません…><
Railsのsprocketsをやるやつです
以前にmiddlemanを少し触ってまして、(エントリはこちら)そのときのasset pipelineがすごい便利でした。で、sprocketsはその根幹になる仕組み?なのかな?
下記のようなコード、見たことありますよね。
//= require hoge.js
//= require_tree somedir
これをgruntのタスクとして動かせるようにしただけの単純なやつです。
「え、そんなんgrunt-contrib-concatでいいやん」
はい、その方がラクなんですが、ちょっと用途が違ってまして。
メインのソースコードがあって、その途中で任意の外部スクリプトを注入したいケース
を想定しています。grunt-contrib-concatの場合は、単純にファイルを結合するだけのものなので、最終的には結合されたファイルをメインファイルとして扱うことが多いですよね。でもgrunt-sprocketsの場合は、すでにメインファイルはあって、その中にファイルをインクルードすることを目的にしています。例えば、匿名関数のクロージャの場合、grunt-contrib-concatだと、
// start.js
(function(global) {
// end.js
})(this);
みたいなファイルを用意する必要があります。jqueryだと、intro.js/outro.jsというファイルが用意されていますね。つまりは、これが面倒じゃないかなってことで。grunt-sprocketsなら、
(function(global) {
//= require a.js
//= require b.js
})(this);
と書くことでビルドができます。どちらがいいかは好みにもよるので一概には言えないと思いますが。慣れてる方でいいんじゃないですかね。
gruntの内部構造について
今回はここの知識が欲しかったので、ここがメインです。目標は、
・処理対象ファイル/ファイルの中身の取得 ・ディレクトリ内のファイルリストの取得 ・Gruntfile.jsで書いた設定の情報の取得 ・コンソール出力系 ・タスク追加方法
あたりです。なお、使ってるgruntのバージョンは0.4.1です。0.3系だと動かないかもしれません。
公式のリファレンスとか、翻訳されている方もいらっしゃるので紹介。
処理対象ファイル/ファイルの中身の取得
grunt.file.xxxで操作できるみたいです。
``` // file.jsを読み込み var data = grunt.file.read('file.js');// ファイル/ディレクトリが存在するかどうかを判定 if ( grunt.file.exists(‘xxx’) ) { // ファイル/ディレクトリが存在する場合の処理 }
// ファイルかどうかを判定 if ( grunt.file.isFile(‘xxx’) ) { // ファイルの場合の処理 }
// ディレクトリかどうかを判定 if ( grunt.file.isDir(‘xxx’) ) { // ディレクトリの場合の処理 }
<p>基本、これでできるかと。注意点は、<strong style="color:red">ファイルパスは、Gruntfile.jsのあるディレクトリからの相対パス</strong>であることです。grunt.file.setBase()で変更もできるようです。</p>
<h4>ディレクトリ内のファイルリストの取得</h4>
<p>grunt.file.expand()を使えばOK。</p>
// srcディレクトリ以下のファイルリストを取得 var files = grunt.file.expand({}, ‘src/*’);
<p>ワイルドカードが使えるので、これで一覧が取得できます。第一引数はいくつかパラメータがあるので、リファレンス参照です。</p>
<h4>Gruntfile.jsで書いた設定の情報の取得</h4>
<p>grunt.config('タスク名');で取得できます。</p>
// ----------------- Gruntfile.js grunt.initConfig({ sprockets : { files: [‘sample/test.js’], dest: ‘out/assets.js’ } });
// ---------------- task/sprockets.js // “sprockets”タスクの設定を取得 var config = grunt.config(‘sprockets’);
console.log(config.dest); // out/assets.js
<p>これは必ず使うでしょうね。</p>
<h4>コンソール出力系</h4>
<p>grunt.log.xxxで出力。これはリファレンスを見てもらうといいです。grunt.log.okxxxで成功系、grunt.log.errorxxxで失敗系のログが色付きで出ます。</p>
<h4>タスク追加方法</h4>
<p>これは勘違いしていました。
タスクファイルの中で、必ずgruntを引数に取る関数をエスクポートしないといけないと思っていたのですが、内部でregiterTaskをすれば何もエクスポートしなくてもOKなようです。</p>
// 初めはこうしないといけないと思っていた module.exports = function(grunt) { // do task };
// これでも良い var grunt = require(‘grunt’);
grunt.registerTask(‘task name’, ‘task description’, taskRunner);
<hr stye="border:solid 1px #d20808">
<p>2013/08/28追記</p>
<p>勘違いを勘違いしていました>< grunt.loadTaskでロードしたタスクファイル内で、module.exportsの結果に関数オブジェクトを返却すると、その関数を実行するようです。なので、今回のケースだと、タスクが2重に走ってしまうみたいです。(gruntのソースコードを確認)
今回はタスクファイルの中でregisterTaskをするので、gruntでロードした場合はmodule.exportsせず、busterでロードした場合は関数を返却するようにする必要があります(テストを実行したいので)。なので、苦肉の策を…。</p>
if ( ! /grunt$/.test(process.argv[1]) ) { module.exports = Sprockets; }
<p>gruntで起動している場合は関数エクスポートを行わないようにしました(;´Д`)もう少しいい方法があると思うので調査。</p>
<hr stye="border:solid 1px #d20808">
<p>ただし、今回はメインコンストラクタであるSprocketsをエクスポートしています(関連のないものをエクスポートしてもOK)。これは後述のように、コンストラクタが戻らないとテストにしくいからです。
こんな感じで、独自ファイル操作系のタスクが作れました(∩´∀`)∩ワーイ
詳しくはソースを見てもらえれば。
</p>
<h2>テストもやるよ</h2>
<p>次はタスクのテストですが、私はbuster.jsを使っているので、こちらのテストケースを書きます。
他のテスティングフレームーワークでもいいんですが、個人的にRSpecのようなテストの書き方にちょっと慣れなくて、普通にアサーションができるテストが好みです…。あ、ちなみにbuster.jsはアサーションするタイプでもRSpecのような書き方のどちらでもできるようで大変優秀です。(使ってる人増えると嬉しいな…)
テストとしては不完全かもですが、とりあえずインストールとテストを書いてみます。
</p>
インストール
npm install buster —save-dev
// buster.js var config = module.exports;
config[‘sprockets’] = { env: “node”, tests: [ ‘tests/sprockets.js’ ] };
<p>続いて、テストケース。</p>
// tests/sprockets.js
var buster = require(‘buster’); var assert = buster.assertions.assert; var refute = buster.assertions.refute; var Sprockets = require(‘../tasks/sprockets’);
buster.testCase(‘SprocketsTest’, { “resolveDepenencyRequire#Success” : function() { var result = Sprockets.resolveDepenencyRequire(‘sample/test.js’); assert.isString(result); }, “resolveDepenencyRequire#Failed” : function() { var result = Sprockets.resolveDepenencyRequire(‘sample/notfound.js’); refute.defined(result); }, “loadDirectoryFiles#Success” : function() { var result = Sprockets.loadDirectoryFiles(‘sample/src’); assert.isArray(result); }, “loadDirectoryFiles#Failed” : function() { var result = Sprockets.loadDirectoryFiles(‘sample/notfound’); assert.equals(result, ""); } });
<p>あとは、package.jsonにテスト設定を書きます(後述のTravis CIのため)。</p>
// package.json
{ “name”: “grunt-sprockets”, — 中略 — “scripts”: { “test”: “./node_modules/.bin/buster-test” }, — 中略 — “devDependencies”: { “grunt-cli”: “~0.1.9”, “grunt”: “~0.4.1”, “buster”: “~0.6.12” } }
<p>"scripts":"test"のセクションにbuster-testへの相対パスを指定します。これでnpm testでbusterのテストケースが走ります。
<img src="/media/42d85bcf56383a02924f7ab8caa7cb8e.png" alt="shot1" title="動いてるねぇ">
最後に、.travis.ymlを書いて終了です(Travis CI初めて触りました)</p>
// .travis.yml language: nodejs nodejs:
- “0.10” before_script:
- npm install script:
-
“npm test”
Travis CI側の設定方法は以下のサイトを参考にさせて頂きました。
感想など
なかなか手を出してなかったんですが、やってみるとサクサクできたので面白いかなー。 社内の独自アプリのビルド用タスクも簡単に作れそうです( ╹◡╹)
あとは、コツコツHaxeを勉強しているので、Haxeで静的にコンパイルチェック+テストでより安全なビルドが作れるようになれば最高ですね。
現場からは以上です。