Docker Node Testerを使ってNode.jsをバージョン毎にテストする #葉桜js
昨日は 葉桜js でした。
今回は客層からかテストとセキュリティの話が多かった。t_wada, malaさんの影響は大きい。 #葉桜JS
— Yosuke FURUKAWA (@yosuke_furukawa) April 14, 2014
僕のLT
t_wadaさんが来るということでテストっぽい話をしたくて、Docker Node Testerの話をしました。
Node.js v0.12 に関して
このブログで Node.js v0.12で変わることについて色々と紹介してきました。NANの話もそうですし、traicing apiとかsync child_processもそうです。
Node.js v0.12では他にもコアモジュールに色々とバグ修正やエンハンスが加わっています。
コアチームが互換性に配慮しているとはいえ、影響を受けてしまい、自分のモジュールが動かなくなることは考えられます。
そこで Docker Node Tester
Docker を使ってNode.jsのバージョン間でビルドしたイメージを作っておいて簡単にバージョンごとのテストを行うツールです。
How to use
インストールは下記のとおりです。
$ npm install dnt -g
.dntrc
っていう下記のようなファイルをプロジェクトのルートに作っておきます。
NODE_VERSIONS="\ master \ v0.11.9 \ v0.11.8 \ v0.10.22 \ v0.10.21 \ v0.8.26 \ " TEST_CMD="\ cd /dnt/ && \ npm install && \ node_modules/.bin/node-gyp --nodedir /usr/src/node/ rebuild && \ node_modules/.bin/tap test/*-test.js; \ "
この状態で sudo setup-dnt
を実行するとNODE_VERSIONSに記述したNode.jsのイメージを作成してくれます。
最後に sudo dnt
を実行すると下記のような結果が出ます。
これで、バージョン間のテストを実現できます。
注意点
まだ Mac の boot2dockerだとメモリ不足の影響でdocker環境構築に時間がかかったり、エラーになることがあります。(※僕はDigital OceanでUbuntuの環境を借りて実施しました。)
また、最新のDocker v0.10に Docker Node Testerが対応できてないので、今pull reqを投げて様子を見ているところです。既にDocker v0.10がインストールされているのであれば、僕のpull req内容を元に試してください。
pullreqがマージされたので、最新のdocker node tester v0.3.0 以降では docker v0.10サポートされました!!
#寿司js 、#桜js に行ってきた感想と居心地が良いコミュニティを作ることに関して
teppeisさんのブログエントリを読んで、そういえば全然寿司jsと桜jsについて書いてなかったなと思ったので書く。
寿司js
寿司jsはいつもtwitter上では絡みのあるteppeisさんと一回話してみたかったazu_reさん、imayaさんと飲んでみたいというモチベーションから寿司食べに行こうって誘ったのが始まりでした。結局飛行機の都合でteppeisさんは来れなかったものの、hokacchaさんやyoshikikojiさんやkyo_agoさんが集まってワイワイした飲み会LT大会に。
今必死で寿司用のブログを書いてる。ちなみに寿司.jsというのは、僕がteppeisさんとazu_reさんにライブラリ情報更新ツイートいつも感謝してますという旨の連絡したらteppeisさんから寿司を強請られたので、開催した飲み会です。飲み会のはずだったんやで。。 #寿司js
— Yosuke FURUKAWA (@yosuke_furukawa) March 31, 2014
寿司jsは以下のエントリを参考にしてもらえればいいかと。
寿司jsでしたLT
僕が寿司jsでした話は、チーム開発してて依存ファイルに変更あった時にライブラリインストールし直す必要あるんだけど、みんなどうしてんの?っていう話で、ちょうど事前にブログ書いてたので、それを解決するためのツール、hookinの紹介でした。
git pullでファイルに変更があったら特定のコマンドを実行する。 - from scratch
毎回起動する時にインストールしなおして解決するとか、代替策は出たんだけど、弊社ではgithubにはあるけど、npmにpublishされてないモジュールが多くて、そういうモジュールをpackage.jsonに記述すると、npm が変更があったかどうかを検知できずに毎回installが実行されるから無駄が多いんだという議論になり、この辺、npmが頑張ってくれるといいよね、っていう話をした。
寿司jsの感想
Promise、package.jsonの話、WebComponentsの話、結婚式LTの話、dockerの話、Firefox、vuejs、hueの話と盛り沢山ですごく楽しかった。ガチなトークでした。参加できなかった人 (@teppeis 含む)は第二回で会いましょう
#寿司js
— Yosuke FURUKAWA (@yosuke_furukawa) March 31, 2014
まぁこんな感じだった。どちらかと言うとお互いが共通項目について深く話すというよりもお互いの業界を肴に便利情報や自分の興味を語り合うって感じだった。
LTの内容は全部面白かったんだけど、特に面白かったのは、mozaic.fmでも話してたWebComponentsの話 by hokacchaが面白かった。
皿属性消すとお皿見えなくなる #寿司js pic.twitter.com/AdafxYGyy7
— Yoshiki Kojima (@yoshikikoji) March 31, 2014
x-sushiっていうカスタムタグを作って皿属性を消すと皿を見えなくしたり、うにをマグロに変えたりとデモをしてくれた。モジュラビリティを高める話もあったし、ちょうどmozaic.fmで話してたパッケージマネージャの話もチラホラ出てて面白かった。
桜js
花見しながらハッカソンしたいよね、っていうノリからkyo_agoさんが主体となって呼びかけてくれた飲み会LT大会です。結局天候の都合によって花見ハッカソンは叶わなかったんだけど、居酒屋でLT大会になった。
寿司jsの時にtkihiraさんに会いたいって言っていた人が多数いたのでtkihiraさんを入れ、飛び入りでJxckさんとotiai10さんを交えて開催した。
桜jsでしたLT
僕が桜jsでした話はNode.js v0.12で変わることの話だった。tracing apiやchild_processのsync系 APIの話、つい最近入ったPromise, WeakMap, Object.observeが使えるようになるっていう話をした。
基本的には前のNode学園で発表した話と当日にあったpull reqの話だった。
これからのNode.jsの話をしよう // Speaker Deck
https://github.com/joyent/node/pull/7394#issuecomment-39276195
tracing apiとかWeakMapとかメモリの解放がやりやすくなっていいよね、っていう話をした。
桜jsの感想
#桜js おもしろかった、さくらのクラウド、node v0.12、WebGL、HTMLのレイアウトとWebの定義、coffeescriptの踏み絵、closure compiler、formタグの今後、MクライアントサイドMVC dis、power assertの話が出た。
— Yosuke FURUKAWA (@yosuke_furukawa) April 2, 2014
みんな今後のWebについての話がすごく熱かった。みんな背景違うのにcoffeescriptの時だけ一つになれた感があった #桜js
— Yosuke FURUKAWA (@yosuke_furukawa) April 2, 2014
というわけで、teppeisさんが書いてくれている通り、深い議論ができて物凄く面白かった。
みんながしたLTと問題提起が良かったんだと思う。
居心地が良いコミュニティを作ること
桜jsと寿司js、すごく居心地が良かった。コンテキストが大体一緒で、皆一定以上の深い造詣や知識があってそういう人達が集まってワイワイ議論するのって物凄く楽しくて有効で、大きめの勉強会行って話し聞いて帰ってくるよりも得られたものは大きかったと思う。
PUT/DELETEの議論の深化もそうだし、クライアントサイドMVCに対するdisもそう。気を遣う事無く自分の思っていることをぶつけられる場っていうのはやっぱり楽しい。
んで、自分は今年の1月からNode.jsユーザーグループの代表なんですけど、Node.jsってフロントエンドツールとしてもバックエンドサーバとしても使われてて、活躍の舞台が広くて必然的にたくさんの人が集まってくれてて嬉しい限りなんですが、大人数になればなるほどコンテキストが大体一緒で深い知識や造詣を持っている人達とコミュニケーションするのってどんどん難しくなってきてるなと感じています。
しょうがないんですけど、東京Node学園に来てもらって発表だけ聞いて帰った時に得ているものと桜js、寿司jsで深い議論した時に得ているものって違うんですよね、質も量も。
Node学園で発表している内容ってのは、そこまで本質とは関係なくて、今のNode.jsで起きている最新の事をsyncする程度で考えてもらえれば良いかなと思っているんです。本質は最後にある懇親会だと思うんですよ。そこで自分が今抱えている問題や作ろうとしているアプリケーションのことを知見者に話したり、深い造詣を持っている人に聞くっていう、思っていることをぶつけられる場にしたいなと思うんです。
発表やLTはそのためのものであって、コミュニケーションのためのタネです。時間の都合上しょうがない人も多いと思うし、人見知りの人もいるのでしょうがないと思うんですけど、発表だけ聞いて懇親会に出ないというのは自分の思っていることをぶつけられる機会を失っているって言うことなので非常に損だと思うんです。
という訳で、今度Node学園 4/24(木) 19時から渋谷ヒカリエで実施します。
コミュニケーションしてみたいけど、詳しい人が誰なのか分からないって言う方や知り合いがいないっていう人は僕が懇親会で話している輪に勝手に加わって、僕に質問や思いの丈をぶつけて下さい。僕より詳しい人がいればその人を紹介しますし、答えられる範囲で話します。
I'm looking forward to talking with you :)
node.jsのnative addonを作るときはNANを使おう。
さて、 Node.js v0.12 で変わることの一つとして、native addonを作る時に後方互換性を壊す変更が加えられています。
これにより、v0.10でnative addonを作っているモジュール達は、ほとんど動かなくなってしまうことが考えられます。
V8側がこの後方互換性を壊す変更をしているため、V8に追従しているNode.js側としてはこのbreaking changesを受けざるを得なかったんだと思います。*1
どれくらい変更されてるのかは node.js の native addon で Hello World モジュールを作る方法が載っているのでそれをまずは参考にします。
// これまでの v0.10ではこう書いてた。 #include <node.h> #include <v8.h> using namespace v8; Handle<Value> Method(const Arguments& args) { HandleScope scope; return scope.Close(String::New("world")); } void init(Handle<Object> exports) { exports->Set(String::NewSymbol("hello"), FunctionTemplate::New(Method)->GetFunction()); } NODE_MODULE(hello, init)
// v0.11以降はこう書く #include <node.h> using namespace v8; void Method(const FunctionCallbackInfo<Value>& args) { // 現在のisolate(current thread)を取得する。 Isolate* isolate = Isolate::GetCurrent(); HandleScope scope(isolate); // args.GetReturnValueで値をreturnする。 // Stringのインスタンス方法も変更されてる。 args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world")); } void init(Handle<Object> exports) { // NODE_SET_METHODというFunctionTemplateのヘルパー関数を利用してる。 // これ自体は v0.8から既にあった様子。 NODE_SET_METHOD(exports, "hello", Method); } NODE_MODULE(addon, init)
とまぁこうなります。
まぁお世辞にも変更が少ないとは言えないような感じです。
このHello Worldのモジュールだけでも、かなり変更されている事がわかります。
これって
V8に追従している以上しょうがないんですけど、ネイティブモジュールの作成者側からするとかなり辛いです。
しかも一度この変更に対処してしまうと、v0.10ではそのモジュールの最新に追従できないという問題も発生します。
というわけで、毎回毎回辛い思いをしているんですが、そんな状況に終止符を打つためのNANという抽象化ラッパーが登場しています。
NAN (Native Abstractions for Node.js) とは
node.js NAN で検索してもNaNの事しかヒットしないのでググラビリティとは何なのか、という名前ですが、提供してくれる機能は強力です。
先ほどの breaking changes に対応してくれるためのモジュールで、NANが提供するラップされた関数を使ってnative add-onを作っておけば、NANがnode.jsのバージョンを見て、v0.8, v0.10, v0.11のいずれかの変更に対応してくれます。先ほどの問題を解決してくれるわけです。
NANというのは、Native Abstractions for Node.jsの略称です。直訳するとNode.jsのためのネイティブ抽象化、という意味ですね。
作者のメッセージを引用します。
Thanks to the crazy changes in V8 (and some in Node core), keeping native addons compiling happily across versions, particularly 0.10 to 0.11/0.12, is a minor nightmare. The goal of this project is to store all logic necessary to develop native Node.js addons without having to inspect NODE_MODULE_VERSION and get yourself into a macro-tangle.
This project also contains some helper utilities that make addon development a bit more pleasant.
V8 (と Node coreのいくつかのモジュール) の crazy な変更のおかげで、native addonがバージョン間を跨いでコンパイルするのは困難になってしまったよ。 特に 0.10-0.11/0.12のマイナーチェンジは悪夢だね。
このプロジェクトのゴールはNode.jsのバージョン変更を調査しなくても、native addon を開発するために必要なロジック、ライブラリを提供することだ。このプロジェクトには、バージョン間の差異を吸収するだけじゃなく、addon 開発をより楽しくするためのヘルパーユーティリティも含むよ。
ということです、NANをベースに作っていきましょう。
NAN 対応する
とりあえず、HelloWorldをNAN対応しましょう。
まずはインストールから
$ npm install nan -S
installしたらbinding.gypのtarget property以下に下記の文字列を記述します。
"include_dirs" : [ "<!(node -e \"require('nan')\")" ]
終わったら、以下のようにnativeモジュールをNAN対応していきます。
addon.cc
#include <node.h> // nan.hを読み込む #include <nan.h> using namespace v8; // v8に公開したいMETHODの定義時にNAN_METHODを使う NAN_METHOD(Method) { // NanScopeで始める、これはHandleScopeをラップする奴。 NanScope(); // NanReturnValueで値を返す、return のラッパー // ちなみに文字列もNanSymbolで囲む NanReturnValue(NanSymbol("world")); } void init(Handle<Object> exports) { // exports->Setで定義する方向に固定。 // その代わりNanSymbolを使う exports->Set(NanSymbol("hello"), FunctionTemplate::New(Method)->GetFunction()); } NODE_MODULE(addon, init)
ここまででaddon.ccモジュールのNAN対応が実施されました。
NAN対応済みのサンプルプロジェクトは以下になります。
yosuke-furukawa/NANSample · GitHub
これを使って実際にv0.8 - v0.11で使えるのかtravisで確認しました。
結果はこんな感じで全部のバージョンでビルドができて、テストが通る事を確認できました。
Hello World以上の事を知りたい場合は
NANプロジェクトにあるexamplesが参考になります。円周率を求めるためのモジュールですが、syncのAPIとasyncのAPIがそれぞれ用意されてて非常に参考になります。
後はsindresorhusさんが、WindowsとOSXの時の差を吸収するためのモジュールの書き方を実践してくれているのでそれも参考になるかもしれません。
2014/04/08 追記 NAN対応を実践してみた。
実践してみました。ハマりどころを紹介しておきます。
NAN対応でハマった所:
通常のコードをNANでラップするのはそこまで難しくなかった。
一応、雰囲気としては、HandleScope scope;
から始まるメソッドの始まりはNanScope();
にして、returnで返す値はNanReturnXXXX
を使う、
jsに公開するメソッド Handle
等はNAN_METHOD(Hoge)
に変更するだけ。
という感じにやっていけば機械的に変更できる。
PersistentからLocalにする時に一瞬ハマったけど、公式ドキュメントにあるNanPersistentToLocal
を使えば大丈夫。
大変だったのは、、、
- v8::StringでMayContainNonAsciiとWriteAsciiの2つのメソッドが消えていること
- ArrayBufferクラスのインスタンスの取り扱いが変わっていること
の2点。
MayContainNonAsciiとWriteAsciiの2つのメソッドが消えている
MayContainNonAsciiとWriteAsciiはバグってた様子。共通で使っているHasOnlyAsciiCharsっていうメソッドが正しい値を返さないことがあるってことで、APIから削除されました。
んで、新しくIsOneByteっていうメソッドが追加されているのでv0.11以降はそれを使いましょう。v0.10ではまだこのメソッドが追加されていないので、そこは諦めてこうやって書きましょう。
#if (NODE_MODULE_VERSION > 0x000B) // node.js v0.11+ bool hasMultiByte = !str->IsOneByte(); #else // node.js v0.10 bool hasMultiByte = str->MayContainNonAscii(); #endif if (hasMultiByte) {
WriteAsciiはテストコード読むとWriteUtf8を使うようになったので、そちらを使いましょう。
ArrayBufferの取り扱いが変わった件
Bufferから値を読み込む時に値が入っているかどうかを確認するために使う、HasIndexedPropertiesInExternalArrayData
がtrueを返さなくなった。
詳しくは、このgist見てもらうといいんですけど、ArrayBufferを渡すのではなく、TypedArrayそのものを渡すと動いたので、そういう動作に変更されたようです。
というところでかなりハマったんですが、なんとか抜け出せました。:)
*1:Node.jsが安全性を担保する上で要求した変更も多いようなので、breaking changesに関してはV8だけを攻めるのは筋違いですね。 see: https://groups.google.com/forum/#!msg/v8-users/6kSAbnUb-rQ/e-GI0b1ThA4J
testling-ciとtravis-ciでクライアントサイドもサーバサイドもテストを実行する
まえがき
NHK番組表APIのjavascript版を作りました。
NHKの番組表APIが発表されてて、皆思い思いに好きな言語で実装されていくのを見てました。
Golang : https://github.com/mattn/go-nhk
Scala : https://github.com/seratch/nhk4s
Perl : https://github.com/moznion/WWW-NHKProgram-API
Python : https://github.com/drillbits/nhk-api
Ruby : https://github.com/mitukiii/nhk_program-for-ruby
Emacs : https://github.com/gongo/emacs-nhk-program
Titanium : https://github.com/h5y1m141/TiNHKProgram
それのNode.js版ってよく見ると無いなと思ったんで作ったんですが、これは考えてみればbrowserifyでクライアントサイドでも使えるライブラリとして公開するチャンスだと思ってクライアントサイドjavascriptとしてもサーバーサイドのjavascriptとしても動作するハイブリッドのNHK番組表クライアントを作ってみました。
Node.js : https://github.com/yosuke-furukawa/nhk_api.js
npm : https://www.npmjs.org/package/nhk_api
bower : http://bower.io/search/?q=nhk_api
Getting Started
node.jsの場合
$ npm install nhk_api
browserから使う場合
$ bower install nhk_api
installしたらbowerをscriptで読めるようにする。
<!-- in browser --> <script src="components/nhk_api/client/nhk_api.js"></script>
使い方
var NHK = require("nhk_api"); // KEYを設定してください。 var nhk = new NHK("YOUR_API_KEY"); var callback = function(err, result) { console.log(JSON.stringify(result)); }; // nhk list api nhk.list.get("130", "g1", callback); nhk.list.get("東京", "NHK総合1", callback); nhk.list.get("東京", "NHK総合1", "tomorrow", callback); // nhk genre api nhk.genre.get("130", "g1", callback); nhk.genre.get("東京", "NHK総合1", callback); nhk.genre.get("東京", "NHK総合1", "tomorrow", callback); // nhk info api nhk.info.get("130", "g1", "123456789", callback); // nhk now on air api nhk.now.get("130", "g1", callback);
本題
さて、これは単に作ったというだけで本題はここからなんですが、Node.js版、というかサーバーサイドはtravisとかあって簡単にバージョンを跨いだテストが出来ますよね。
クライアントサイドの場合はどうでしょう。ブラウザ間のバージョンを跨いだテストを実施したいところです。せっかくbrowserifyを使ってサーバーサイドとクライアントサイド両方共同じ使い方で使えているので、testling-ciというサイトを使ってブラウザ間のテストを実行しましょう。
testling-ciでテストを実行すると以下の様なバッジが生成されて見れるようになります。
どのブラウザでサポートされているのか一目瞭然で分かりやすいですね。
ちょっと前にsubstackが公開していた記事があって、それを見るとやり方が書いてあります。ちょっと長いんですが最後までお付き合いをお願いします。
簡単にやり方を紹介していきます。
テストツールのインストール
以下のツールをインストールしておきます。
// phantomjs は必須、そうでなくても何らかのヘッドレスブラウザがないとtestlingが動かない $ brew install phantomjs $ npm install browserify tape testling -g
browserifyはサーバーサイドのコードをクライアントでも使えるようにしてくれるツールですね。
tapeはtapが喋れるテストライブラリです、mochaでもいいんですが、substackが最近issue全部クローズした!!って騒いでたので使ってみました。
no more open issues on tape! https://t.co/WgktE9XWxh
— James Halliday (@substack) March 5, 2014
まずはtapeを使ってテストを書いて、nodeのテストを実行します。
こんな感じのファイルをtestフォルダ以下に用意します。
var NHK = require('../index'); // tapeをrequire var test = require('tape'); // 時間用ライブラリを利用 var moment = require('moment-timezone'); // this apikey for test var apikey = process.env.NHK_API_KEY || "123456789"; // genreのurlがあっているかどうかテスト test(' genre url ', function (t) { var nhk = new NHK(apikey); var url = nhk.genre.createUrl("130", "g1", "0000"); var date = moment().tz("Asia/Tokyo").format("YYYY-MM-DD"); // nhkのgenre apiのurlと合致することを期待 var expected = "http://api.nhk.or.jp/v1/pg/genre/130/g1/0000/" + date + ".json?key=" + apikey; // t.equalで比較、第一引数が actual、第二引数が expected、第三引数がコメント t.equal(url, expected, "url is same"); // t.endでテストが終了される。 // t.endを呼ぶのは必須、非同期テストの場合はこっちのほうが嬉しい。 t.end(); //ちなむとt.planっていうメソッドがあって、それをcallすると指定回数のassetチェックが終わると自動でendされる仕組みになる。 // t.plan(1); }); test(' genre url to specify keyword ', function (t) { var nhk = new NHK(apikey); // 日本語でもgenre生成されるんですよ var url = nhk.genre.createUrl("東京", "NHK総合1", "0000"); var date = moment().tz("Asia/Tokyo").format("YYYY-MM-DD"); var expected = "http://api.nhk.or.jp/v1/pg/genre/130/g1/0000/" + date + ".json?key=" + apikey; t.equal(url, expected, "url is same"); t.end(); });
テストの実行は簡単で
$ tape test/genre.js
で実行できます。
TAP version 13
# genre url
ok 1 url is same
# genre url to specify keyword
ok 2 url is same
# genre url today
ok 3 url is today
# genre url tomorrow
ok 4 url is tomorrow
# get genre url
ok 5 error is not found
ok 6 msg is truthy
ok 7 msg.list is truthy
ok 8 msg.list.g1 is truthy1..8
# tests 8
# pass 8# ok
node-tapをインストールしているならtap
コマンドが使えるようになるので、以下の様な感じになります。
$ tap test/genre.js ok test/genre.js ........................................ 9/9 total ................................................... 9/9 ok
と、ここまでは普通のNode.jsのテストですね。
testlingを使ってbrowserのテストも実行する。
testlingとbrowserifyを使ってbrowser側のテストも実行しましょう。実行はすごく簡単。
$ browserify test/genre.js | testling TAP version 13 # genre url ok 1 url is same # genre url to specify keyword ok 2 url is same # genre url today ok 3 url is today # genre url tomorrow ok 4 url is tomorrow # get genre url ok 5 error is not found ok 6 msg is truthy ok 7 msg.list is truthy ok 8 msg.list.g1 is truthy 1..8 # tests 8 # pass 8 # ok
ってやるだけですね、これはすごく単純なことしかしてなくて、
- browserifyでtestコードをブラウザでも実行できるようにする
- testlingからphantomjsを呼び出してそのコードを実行する
ということをやっているだけです。
※phantomjsを入れる前にtestlingを既にインストールしていると、phantomjsを入れてもtestlingがphantomjsを認識してくれない事があります、この場合は ~/.config/browser-launcher/config.json
を一旦消してから再実行してみましょう。
coverageを取る場合
substackが作っているcoverifyっていうツールを使います。
$ browserify -t coverify test/*.js | testling | coverify ... # /Users/yosuke/Program/nhk-api/lib/base.js: line 53, column 9-28 console.error(data); ^^^^^^^^^^^^^^^^^^^^ # /Users/yosuke/Program/nhk-api/lib/base.js: line 54, column 9-25 console.error(e); ^^^^^^^^^^^^^^^^^ # /Users/yosuke/Program/nhk-api/lib/base.js: line 55, column 9-14 cb(e); ^^^^^^ # /Users/yosuke/Program/nhk-api/lib/base.js: line 59, column 5-10 cb(e); ^^^^^^ # coverage: 860/864 (99.53 %)
コレを使うと通っていないコードとテストのcoverageを取ってくれます。この場合は99.53%のカバレッジですね。
tapeとbrowserifyとtestlingでサーバーサイドもクライアントサイドも同じテストコードでテストできるようになりました。ついでにcoverify使うとカバレッジも計測できます。
CIツールで自動テスト
サーバサイドのCIツールはtravisがあるのでそれを使いましょう。以下のサイトが参考になります。
http://d.hatena.ne.jp/hokaccha/20111110/1320910718
クライアントサイドはCIツールとして、testling-ciを使います、これを使うための前準備として、package.jsonに以下のように記述します。
{ "name": "nhk_api", "version": "0.1.1", "description": "Node.js client for NHK API", "main": "index.js", "scripts": { "test": "tape test/*.js", "build": "browserify -g uglifyify -r ./index.js:nhk_api -o client/nhk_api.js" }, "keywords": [ "nhk", "api" ], "author": "yosuke furukawa", "license": "MIT", "dependencies": {}, "devDependencies": { "tape": "~2.10.2", "uglifyify": "~1.2.3", "browserify": "~3.32.0", "moment-timezone": "0.0.3" }, "testling": { "files": "test/*.js", "browsers": [ "ie/6..latest", "chrome/22..latest", "firefox/16..latest", "safari/latest", "opera/11.0..latest", "iphone/6", "ipad/6", "android-browser/latest" ] } }
こんな感じで、testlingプロパティを設定してください。ここにbrowserのどのバージョンをtestling-ciでテストするかを定義します。
設定したら、githubのwebhookページから、http://git.testling.com
へのhookを設定してください。
git pushを実行したら、以下のページで実行が確認できます。
https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME
実際の状況はこちら:https://ci.testling.com/yosuke-furukawa/nhk_api.js
バッジを見る
バッジはこんな感じで手に入ります。
https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME.png
実際のコード
markdownに貼るならこちら
[![browser support](https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME.png)
](https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME)
atomのpackageの作り方
先日、atomというgithub製のIDEが公開されて話題になってます。
これ、広める戦略がうまくて、昔のgmailと同じく、inviteを受けた人が3人だけinvite ticketを持ってて、その人からまた3人inviteできるって仕組みになってます。こうすることでSNSでのinvite ticket要求が盛んになり、流行ってるように見えるというのが上手い。
ちなみにDLされるファイルだけ他人に送っても内部的にチェックしててpackage managerとかが使えない仕組みになってるので、inviteを持ってない人はおとなしく誰かから回ってくるのを待ちましょう。
本題
atom自身はSublime Textっぽい外観で、apmっていうパッケージマネージャが付属されてます。
んで、早速apmに自作のpackageを作って公開してみました。
実行している所:
yosuke-furukawa/language-jsx · GitHub
このlanguage-jsxでは、JSX(DeNA製)のsyntaxハイライト機能、jsxのsnippet機能とPackages > JSX > run から 開いてるjsxファイルを実行できるようにしました。
せっかくなので、作り方と公開方法を説明していきます。
まずはpackageの構造を理解しよう
my-package/ grammars/ - 文法を定義して、シンタックスハイライトする keymaps/ - keybindを定義する lib/ - いわゆるライブラリ、大体ココに機能を定義する menus/ - メニューの表示を定義する spec/ - test用フォルダ snippets/ - スニペット、短縮キーワードを登録可能 stylesheets/ - cssでページのスタイル決めるところ package.json - そのpackageの定義を記述する、npmと同じような書き方
こんな感じのフォルダ構造になります。
npm
を作ったことがある人なら分かるかと思いますが、package.json
というpackageの定義を記述するファイルが必要です。
このフォルダ群の雛形は Packages > Package Generator > Create Atom Package を実行すると簡単なフォルダ群が作成されます。
適当な名前をつけて実行すると、~/.atom/packages
に以下のようなフォルダ群を作ってくれるので楽。
my-package/ keymaps/ lib/ menus/ spec/ stylesheets/ .gitignore LICENSE.md README.md package.json
なので、ここから作業を始めて行くといいと思います。
まずは package.json を確認
package.json
{ "name": "my-package", "main": "./lib/my-package", "version": "0.0.0", "private": true, "description": "A short description of your package", "activationEvents": ["my-package:toggle"], "repository": "https://github.com/atom/my-package", "license": "MIT", "engines": { "atom": ">0.50.0" }, "dependencies": { } }
多分こんな感じの外観になっていると思います。
package.jsonの中身を少し説明しておきます。
この中で重要なのは
- main (指定必須) entrypointへのパスを指します、これがないと正常にインストールされないので指定必須です。
- activationEvents (任意指定) packageがロードされてから有効になるイベントです。遅延ロードする時に使います。
- dependencies (任意指定) ここにnpmのパッケージを指定することができます、今回作成した
language-jsx
の場合、jsxの依存があるので、以下のようになっています。
"dependencies": { "jsx": "*" }
全体としては以下のようになっています。
{ "name": "language-jsx", "version": "0.1.3", "main": "./lib/jsx", "description": "JSX language support in Atom", "engines": { "atom": "*", "node": "*" }, "activationEvents": [ "jsx:run" ], "dependencies": { "jsx": "*" }, "repository": { "type": "git", "url": "https://github.com/yosuke-furukawa/language-jsx.git" }, "license": "MIT", "bugs": { "url": "https://github.com/yosuke-furukawa/language-jsx/issues" } }
ちなみにsnippetsやstylesheetsのフォルダ名を変えるなら、このpackage.jsonにsnippetsとかstylesheetsとかでキーを指定して、フォルダ名を指定すれば変更できます。
{ "name": "language-jsx", "version": "0.1.3", "main": "./lib/jsx", "description": "JSX language support in Atom", "engines": { "atom": "*", "node": "*" }, "activationEvents": [ "jsx:run" ], "dependencies": { "jsx": "*" }, "repository": { "type": "git", "url": "https://github.com/yosuke-furukawa/language-jsx.git" }, "license": "MIT", "bugs": { "url": "https://github.com/yosuke-furukawa/language-jsx/issues" }, "snippets" : "./abc", "stylesheets": "./css" }
grammarsを定義しよう
TextMateでもあったと思いますが、Grammarを定義しておくことでsyntaxのハイライトができるようになります。
定義例:grammars/jsx.cson
{ 'match': '(?<!\\.)\\b(boolean|byte|char|class|double|enum|float|function|int|interface|long|short|void)\\b' 'name': 'storage.type.jsx' } { 'match': '(?<!\\.)\\b(const|export|extends|final|implements|native|private|protected|public|static|synchronized|throws|var)\\b' 'name': 'storage.modifier.jsx' }
こうすることで、var
とかclass
とかそういうキーワードが修飾語としてハイライトされます。
詳しい定義方法はこちら。
TextMate Manual » Language Grammars
keymapsを定義する
キーマップを定義します。こうすることで、ショートカットキーによるコマンドが定義されます。
定義例:keymaps/jsx.cson
'.workspace': 'ctrl-r': 'jsx:run'
これで、ctrl+r
で'jsx:run'
コマンドを実行するようになります。
ちなみに、'.workspace'
はdivタグに定義されているclassで、全体を表すんですが、特定の状態の時にだけ行いたい場合は、そのクラスを指定するといいです。
'.tree-view-scroller': 'ctrl-V': 'changer:magic'
これでtree-view-scrollerクラスの上でだけctrl-vが有効になります。
menusを定義する
定義すると画面上部のメニュー一覧から実行できるようになります。
定義例:menus/jsx.cson
'menu': [ { 'label': 'Packages' 'submenu': [ { 'label': 'JSX' 'submenu': [ { 'label': 'RUN' 'command': 'jsx:run' } ] } ] } ]
これで、Pacakges > JSX > RUN
を実行すると、jsx:run
が実行されます。
snippetsを定義する
snippetsを定義することで、記述している最中にtab
を押すと補完してくれる機能が増えます。
定義例:snippets/jsx.cson
'.source.jsx': 'class': 'prefix': 'class' 'body': 'class ${1:class_name} {\n\n}' 'main': 'prefix': 'main' 'body': 'static function main(args : string[]) :void {\n\t${0:// body...}\n}'
これで、class
と打ってからtab
を打つとclassの定義が補完されます。
また、main
と打ってからtab
を打つと、static function main(args: string[]) :void {}
が補完されます。
libを定義する
このlibでパッケージの機能を定義します。今回のlanguage-jsx
では、JSXの実行を行う機能を提供します。
jsx_bin_path = "/node_modules/jsx/bin/jsx" child_process = require 'child_process' module.exports = # jsx:runで呼び出された時にrunメソッドを実行するようにする activate: (state) -> atom.workspaceView.command "jsx:run", => @run() # ATOMのSHELLをNodeコマンドとして実行するための環境変数をONにする # thanks @mootoh getExecPath: -> "ATOM_SHELL_INTERNAL_RUN_AS_NODE=1 '#{process.execPath}'" # 外からnodeコマンドの環境変数を渡せるように getNodePath: -> atom.config.get("language-jsx.nodepath") # 実際のmain処理 run: -> # 現在開いているeditorの本体 editor = atom.workspace.getActiveEditor() # language-jsxのパッケージパス lang_jsx_path = atom.packages.resolvePackagePath("language-jsx") # jsxの実行コマンドまでのパス jsx_bin = lang_jsx_path + jsx_bin_path # nodeのコマンドパス node_path = @getNodePath() || @getExecPath() # getUriで現在開いているファイルのパスを取得する uri = editor.getUri() # node jsx --run file_name.jsx command = "#{node_path} #{jsx_bin} --run #{uri}" options = { "cwd" : lang_jsx_path } # 子プロセスから外部コマンド実行する child_process.exec(command, options, (error, stdout, stderr) -> console.error(error) if error console.error(stderr) if stderr console.log(stdout) if stdout ) # DevToolを開く atom.openDevTools()
nodeの外部コマンド呼び出しでハマったこと
jsxのコマンドはnode.jsのスクリプトで出来ているので、nodeから実行することが可能です、ただ、Atomそのものも中身はnode.jsなので、node.jsがmacにインストールされていなくてもAtomさえインストールされていれば実行できるようにしたい所です。
ただ、Atomのprocess.execPath
を取るとnodeを直接使用しているのではなく、 "Atom Helper"
と呼ばれるコードを実行していることが分かります。
これから JSXを呼びだそうとしても、atomが起動してしまい、うまくいきません。
そこでどうするかというと、ATOM_SHELL_INTERNAL_RUN_AS_NODE
という環境変数があることが中身のbinaryファイルから分かるので、その環境変数を無理矢理いじるとatomではなく、atomが内部で使っているnodeをnodeとしてそのまま実行することが可能です。
Atom バイナリを Node として CLI から起動する方法: `ATOM_SHELL_INTERNAL_RUN_AS_NODE=1 Atom.app/Contents/MacOS/Atom` > @yosuke_furukawa
— Motohiro Takayama (@mootoh) February 28, 2014
バイナリを解析してくれたのは @mootoh さん、感謝です。
atomのpackageを公開する。
ここだけはコマンドで実行します。
$ cd my-package $ apm publish
で公開されます。んで、一度公開した後に修正したいときなどは
$ apm publish patch
ってやると勝手にpackage.jsonのversionのpatchバージョンをincrementしてくれます。
minorバージョンアップとmajorバージョンアップもあるので、その辺の詳しい紹介はapm helpを実行して下さい。
$ apm help
東京Node学園 11時限目を開催しました。
東京Node学園の11時限目を開催しました。
新代表に就任してから最初のNode学園ということで結構ハラハラ・ドキドキしてたんですが、
スタッフの皆さんと手伝ってくれた方々のおかげで滞り無く進行することができました。
ありがとうございました。
トゥギャってくれた方がいて、大変ありがたかったです。
イベントの様子はそこ見ても分かるかもしれません。
さてさて、東京Node学園のレポート書いていきます。
「これからのNodeの話をしよう」 by @yosuke_furukawa
内容としては、Node.jsのnews、Node v0.12で変わること、最近のライブラリ、東京Node学園の今後を話しました。
Node v0.12のtracing apiやexecSyncのデモ、koaやPrimusに関して、「知らなかった」とか、「楽しそう!」みたいな反応がツイートから見られたので良かったです。
※ Primusをずっと "プリマス" って読んでたんですが、正しい発音は "プライマス" ですね。
あと、最後の襲名披露口上は超滑りましたが、まぁまぁ楽しんでもらえたようなので、代表としてエンタメ面での仕事ができて良かったかなと思います。
#tng11 滑ったわー
— Yosuke FURUKAWA (@yosuke_furukawa) February 26, 2014
すべりふぁいしたからなー。 #tng11
— Yosuke FURUKAWA (@yosuke_furukawa) February 26, 2014
んで、もっとNode学園にどんな興味があるのか教えて欲しいので、gitterとgithubを作って公開しました。
https://gitter.im/yosuke-furukawa/TokyoNodeFestival2014
https://github.com/yosuke-furukawa/TokyoNodeFestival2014
ぜひ意見をお寄せ下さい。
「browserifyことはじめ」 by @hitsujiwool
hitsujiwoolさんからのbrowserifyの話。
最近browserifyかなり人気があって、azu_reさんの解説記事や改めて入門記事が出るほど。
実際発表もかなり分かりやすく解説されてて、すごく面白かった。
transformすごいなーと思ったし、周りでどんなbrowserifyライブラリが作られてるのか知れてよかった。
browserifyもっと使っていきたいですね。
「NodeでDeep Learning」by @Lewuathe
Nodeで機械学習をやるというすごくエッジの効いた話。
残念ながらパフォーマンスに難あり、という話でしたが、Node.jsで行列演算とかかなり非同期処理に気をつけないと遅いので大変かもしれません。
Pythonだと行列計算が速い、っていう所には若干ライブラリの差異があるんじゃないかと思いますが、いずれにせよ、機械学習ツールにもNode.jsを使うという新たな使い方の道を示してて面白かった。
「非同期プログラミング養成ギブスとしてのnode.js」by @niryuu
callbackでの非同期プログラミングから、Promiseの話、asyncフロー、yield/generatorの話まで非同期プログラミングの今どきのやり方を説明しつつ、結局どのやり方でも見通しはよくなるものの、例外処理や適切な関数分割は必要で、ちゃんとやらないとどれ使ってもダメ、逆に言えばちゃんと使えば非同期プログラミングの本質が分かる、という話だったかと思います。
これはもうまさにその通りで、どれ使っても非同期プログラミングはこれまでと違うパラダイムなので、同期型のプログラミングと同じ書き方はできないし、それに関してはいろんなツールを知って、適切に使うべき。
callback地獄を▶と表現したのは面白かった。逆くの字の意味だったのねw
Maintainable Gruntfile.js
さてさて、前回の続きです。
オレはgruntのエコシステムに乗って楽をしたい、でもGruntfile.jsが長くなりすぎて辛い、grunt taskが時間がかかりすぎて辛い、という話は話で分かります。また、それに対する色んな解決策もあります。
最近出た、HTML5Rocksで紹介されてたやり方もあるし、いくつか先人の知恵もあるので、解決していきましょう。
Gruntfile.jsが長くなりすぎて辛い時
https://github.com/firstandthird/load-grunt-configを使いましょう。
いろんなtipsを見てきましたが、このライブラリが一番分かりやすく、かつGruntfile.jsをメンテナブルに保つことができます。
load-grunt-configには3つの機能があります。
- grunt pluginの自動ロード機能
- grunt configのファイル分割機能
- grunt register task の外出し機能
grunt pluginの自動ロード機能
Gruntfile.jsを書く時、以下の様な感じでgrunt pluginsを記述すると思います。
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-concat');
npmモジュールをロードしてgruntのタスクを読み込んでくれるために必要な箇所なんですが、ここはgrunt-load-configを使うとバッサリと不要になります。
その代わり、load-grunt-configだけをロードしておく必要があります。
つまり
Before
module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { options: { separator: ';' }, dist: { src: ['src/**/*.js'], dest: 'dist/<%= pkg.name %>.js' } }, uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' }, dist: { files: { 'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } } }, qunit: { files: ['test/**/*.html'] }, jshint: { files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'], options: { // options here to override JSHint defaults globals: { jQuery: true, console: true, module: true, document: true } } }, watch: { files: ['<%= jshint.files %>'], tasks: ['jshint', 'qunit'] } }); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); };
AFTER
module.exports = function(grunt) { // load all grunt tasks! require('load-grunt-config')(grunt); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { options: { separator: ';' }, dist: { src: ['src/**/*.js'], dest: 'dist/<%= pkg.name %>.js' } }, uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' }, dist: { files: { 'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } } }, qunit: { files: ['test/**/*.html'] }, jshint: { files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'], options: { // options here to override JSHint defaults globals: { jQuery: true, console: true, module: true, document: true } } }, watch: { files: ['<%= jshint.files %>'], tasks: ['jshint', 'qunit'] } }); grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); };
grunt libraryの読み込みが不要になってスッキリしましたね。
ここまでなら load-grunt-tasks でも同じことができます。
Gruntfile.jsのタスクを記述する際に load-grunt-tasks プラグインが地味に便利 | 5 LOG
プラグイン毎にgrunt.loadNpmTasks()を追加する必要が無くなるload-grunt-tasksを紹介するよ - Qiita
でもまだまだ長いですよね...
grunt configのファイル分割機能
これ使えばgrunt.initConfigを別ファイルに分割できます。
grunt.initConfigで記述していた箇所を削除して、grunt/xxx.jsに移動させます、つまり、
BEFORE
module.exports = function(grunt) { // load all grunt tasks! require('load-grunt-config')(grunt); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { options: { separator: ';' }, dist: { src: ['src/**/*.js'], dest: 'dist/<%= pkg.name %>.js' } }, uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' }, dist: { files: { 'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } } }, qunit: { files: ['test/**/*.html'] }, jshint: { files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'], options: { // options here to override JSHint defaults globals: { jQuery: true, console: true, module: true, document: true } } }, watch: { files: ['<%= jshint.files %>'], tasks: ['jshint', 'qunit'] } }); grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); };
AFTER
Gruntfile.jsは以下のようになります。
module.exports = function(grunt) { // load all grunt tasks! require('load-grunt-config')(grunt); grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); };
その代わり、gruntフォルダ以下に下記のような設定ファイルを記述します。
grunt/uglify.js
module.exports = { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' }, dist: { files: { 'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } } };
grunt/concat.js
module.exports = { options: { separator: ';' }, dist: { src: ['src/**/*.js'], dest: 'dist/<%= pkg.name %>.js' } }
grunt/qunit.js
module.exports = { files: ['test/**/*.html'] };
こんな感じで。
そうするとgruntフォルダ以下にこんな感じで並びます。
grunt ├── uglify.js ├── concat.js ├── qunit.js ・・・
見通しが良くなりましたね。
grunt register task の外出し機能
外出し機能はここまでやらなくてもいい気がしますが、一応あります。
これを使うとGruntfile.jsは3行になります。
BEFORE
module.exports = function(grunt) { // load all grunt tasks! require('load-grunt-config')(grunt); grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); };
AFTER
module.exports = function(grunt) { // load all grunt tasks! require('load-grunt-config')(grunt); };
んで、grunt-registerTaskもgruntフォルダ以下にaliases.jsとして登録します。
grunt/aliases.js
module.exports = { test : ['jshint', 'qunit'], default : ['jshint', 'qunit', 'concat', 'uglify'] };
※もしも中でどうしてもgruntを使いたい場合(gruntのオプションで読み込むタスクが違うなど)
grunt/aliases.js
var grunt = require('grunt'); module.exports = { default: function(target) { if (target === 'force') { return grunt.task.run(['concat', 'uglify']); } return grunt.task.run(['jshint', 'qunit', 'concat', 'uglify']); } };
これで、Gruntfile.jsが短くなってメンテナンスしやすくなりましたね。
Grunt タスクが遅くて辛い時
まずは時間を測りましょう。どんなときも計測が重要です。
その上でチューニング手法を学びましょう。
時間を計測する
time-gruntを使いましょう。
こんな感じに時間を計測してくれるプラグインです。
module.exports = function(grunt) { // show elapsed time at the end require('time-grunt')(grunt); // load all grunt tasks! require('load-grunt-config')(grunt); };
こんな感じで指定します。
これだけで時間がかかっているタスクがわかります。
意味のないタスクを実行しない
さて、時間がかかっていることが分かったらタスクをダイエットさせていきましょう。
まず、gruntで何度もタスクを実行することってあると思います。その際、サブタスクの実行結果に変化がない奴は一度やれば十分です。
例えば、gruntでimageのminimizeとjavascriptのtestを実施するタスクがある場合、javascriptのファイルに変更があればtestの結果は変わりますが、imageの方は変更がないので実施する必要はありません。
こんな感じで前回と実行元のファイルが変わってないのであれば、実行せずにキャッシュを返すというタスクがgrunt-newer です。
使い方は超簡単で、タスクの先頭に"newer"を付けるだけで実現できます。
grunt.initConfig({ jshint: { options: { jshintrc: '.jshintrc' }, all: { src: 'src/**/*.js' } } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-newer'); // newerを付けるだけ grunt.registerTask('lint', ['newer:jshint:all']);
時間がかかるタスクを並列実行したい時
grunt-concurrentとgrunt-parallelizeの二通りの方法があります。
grunt-concurrentはサブタスクを子プロセスを使って並列実行するタスクです。
grunt-parallelizeは一つのタスクで対象のファイルを分割して、子プロセスを使って並列実行するタスクです。
grunt-concurrentを使う
画像の減色処理とcssやjsの圧縮といろいろ時間がかかるタスクを普通にgruntで実行すると直列で実行されるので、時間がかかります。
時間がかかる処理があるのであれば、それを実行している間に別なタスクを実行させて並列化しましょう。
grunt.initConfig({ concurrent: { target1: ['coffee', 'sass'], target2: ['jshint', 'mocha'] } }); grunt.loadNpmTasks('grunt-concurrent'); grunt.registerTask('default', ['concurrent:target1', 'concurrent:target2']);
こんな感じで使います。こうすると、target1で指定したcoffee, sassタスクとtarget2で指定したjshint, mochaタスクを並行実行できます。
grunt-parallelize
一個のタスクを並行実行したいときに使います。
jshintを使いたいけど、srcファイルが多すぎて時間がかかる、full-test流したいけど、testファイルが多すぎるとかですね。
grunt.initConfig({ jshint: { all: { src: './**/*.js' } }, parallelize: { jshint: { // Run jshint:all task with 4 child processes in parallel. all: 4 }, }, }); grunt.registerTask('default', ['parallelize:jshint:all']);
こうするとjshint:allのsrcのファイル群を2つに分割して並列実行してくれます。(teppeis++)