pruntプラグインの書き方


### この記事を読んで分かること

* pruntプラグインのデザインコンセプト
* pruntプラグインの書き方
* Cakeや他のCLIとのコラボ

## pruntのデザインコンセプト

pruntプラグインは

* a) 配列を捌く関数、または
* b) (a) を返すファクトリー

である。

## pruntプラグインの書き方

例えば `.coffee` だけを戻すフィルターを書きます。

    prunt.read('src/*')
      .then(coffeeFilter) # <== これをかきます
      .then(compileCoffee)

### パターン(a): 配列を捌く関数

    path = require 'path'

    coffeeFilter = (files) ->
      # files = [file1, file2, file3... ]
      files.filter (file) ->
        path.extname(file.filename) is '.coffee'

これを拡大してどんな拡張子にも対応できる様にしますと

### パターン(b): (a) を返すファクトリー

    path = require 'path'

    filter = (extension) ->
      (files) ->
        files.filter (file) ->
          path.extname(file.filename) is extension

使用例

    coffeeFilter = filter '.coffee'

    prunt.read('src/*')
      .then(coffeeFilter) # <== これをかきます
      .then(compileCoffee)

## pruntプラグインの書き方 +α

以上のサンプルで分かる様にpruntではファイルクラスを使ってファイルを抽象化しています。

### ファイルクラス

一つのファイル（正しくはファイルインスタンス）には`filename`、`dirname`、`content`、`isDirty`と四つのプロパティが有ります。`isDirty`はファイルがディスクと同期されているかを表すバイナリフラッグで、コンテンツを変更すれば自動的に旗が立ち上がります。その他は説明する必要が有りませんので省略させて戴きます。

    prunt.read('src/*.coffee')
      .then (files) ->
        files.forEach (file) ->
          file.isDirty is true # false
          file.content = 'foo'
          file.isDirty is true # true
        return files

又、ファイルには`file.rename()`と`file.chdir()`、二つのメソッドを持っており、使う必要性は全く無いが、お役に立てれば嬉しい限りです。

### 非同期処理

ではlessコンパイラを書きましょう。lessコンパイラはコールバックで結果を返してくれます。

    less = require 'less'
    less.render file.content, (result) ->
      file.content = result

では[Q.js](https://github.com/kriskowal/q#combination)を使ってpromiseを生成しましょう。

    Q = require 'q'

    exports.less = (options = {}) ->
      (files) ->
        queue = files.map (file) ->
          Q.nfcall(less.render, file.content) # <= returns a promise
            .then (css) ->
              # promise ですから結果をいじるのも簡単です。
              file.content = css
              file.rename file.filename.replace '.less', '.css'
        return Q.all(queue)

promiseを使っているだけに非同期処理は簡単です、**その簡単さが売りです**。

## Cakeや他のCLIとのコラボ

以下の三つだけ紹介させて戴きます。

* Cake
* Optimist
* Commander

### [Cake](http://coffeescript.org/#cake)
cakeはcoffee-scriptと共にインストールされるビルドツールです。`Cakefile`は`Makefile`のcoffee版です。

```coffee-script:Cakefile
{read} = prunt = require './index'

option '-o', '--output [DIR]', 'directory for compiled code'

task 'build', 'build prunt', (options) ->

  concat = prunt.concat {filename: 'index.coffee', dirname: options.output}
  coffee = prunt.coffee() # use default options
  write = prunt.write() # use default options

  read('src/*.coffee')
    .then(concat)
    .then(coffee)
    .done(write)
```

`cake`を実行するとタスクとオプションが表示されます。

```
$ cake
Cakefile defines the following tasks:

cake build                # build prunt

  -o, --output       directory for compiled code
```

### substack/[node-optimist](https://github.com/substack/node-optimist)
browserifyの中の人が作ったライブラリです

```JavaScript:build.js
#!/usr/bin/env node
var argv, coffee, concat, write;

argv = require('optimist')
  .usage('prunt build tool.\nUsage: $0')
  .demand('b')
  .alias('b', 'build')
  .describe('b', 'build prunt')
  .argv;

prunt = require('./index');

if (argv.build) {

  concat = prunt.concat({
    filename: 'index.coffee',
    dirname: '.'
  });

  coffee = prunt.coffee();
  write = prunt.write();

  read('src/*.coffee')
    .then(concat)
    .then(coffee)
    .done(write);
}
```

実行するとこうなります。

```
$ chmod +x build.js
$ build.js
prunt build tool.
Usage: node ./test.js

Options:
  -b, --build  build prunt  [required]

Missing required arguments: b
```

### visionmedia/[commander.js](https://github.com/visionmedia/commander.js/)
expressとmochaの中の人が作ったライブラリです

```JavaScript:build.js
#!/usr/bin/env node
var coffee, concat, program, prunt, write;

program = require('commander')
  .version('0.1.1')
  .option('-b, --build', 'build prunt')
  .parse(process.argv);

prunt = require('prunt');

if (program.build) {
  concat = prunt.concat({
    filename: 'index.coffee',
    dirname: '.'
  });
  coffee = prunt.coffee();
  write = prunt.write();
  read('src/*.coffee')
    .then(concat)
    .then(coffee)
    .done(write);
}
```

実行するとこうなります。

```
$ chmod +x build.js
$ build.js --help

  Usage: test.js [options]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number
    -b, --build    build prunt
```