pig's diary

何でも忘れるので万年初心者ね

muffin.js を入れてみた(Coffeescript の Cakefile用ライブラリ。)

https://github.com/hornairs/muffin

muffin.js をちょっと使ってみました。(v0.2.6)

muffin.js は、

  • (Node.js で動く。)
  • Coffeescript を書いていて、
  • Cakefile も書いていて、
  • Cakefile で child_process をいくつも作らなきゃいけない人

が幸せになれるライブラリだとおもいます。

Cakefile ?

Cakefile は、Coffeescript で書く Makefile です。プロジェクトごとに1個。複数のtask を定義できる。Java でいう Ant の build.xml だとおもいます。Coffeescript を入れると使えるようになります。
task 名は自由ですが、

$ cake build

とかやると、プロジェクト内でbuild 時にやって欲しい事を、させます。たとえば、 .coffee を .js にしたり、client script を minify したりを、さっとできます。そういうCakefile を書いたら。

で、いろいろやらせたくなる。たとえば。おなじCoffeescript でも、serverside と clientside のコンパイル先は変えたい。client side script を concat してひとつにまとめたい。css も、sass とか less とか stylus を使うので、compile したい。それも開発時に、ファイル編集をwatch して、保存後に必要ファイルをcompile しなおしてくれたらとても嬉しい。

そういうことをちゃんとCakefile で書くのは大変です。大変みたいです。作業ごとに、プロセスを走らせる。watch 対象のファイルを洗い出す。watch して、編集されたら対象のファイルをコンパイルして、minify する。コンパイル先が複数だと、複数のプロセスが必要になる。ふーっ。助けが欲しくなります。

muffin.js ?

muffin.js は、Cakefile でよくやること、ファイルの読み書きとか、Coffeescript のcompile とかをするための、 便利関数を提供してくれる人です。Cakefile の中で muffin=require('muffin') と書いて、使います。

q ?

で、ついでにq についても書きたいのですが、よく知らないんです。q は、npm のパッケージで、Javascript のasync のごちゃごちゃぶりを裏でキレイに管理するためのライブラリです。muffin.js は、ファイル操作のヘルパー関数などの裏で、このq を利用しています。muffin.js のユーザーで、対象ファイルを2回以上操作する人は、このq を利用してコールバックを実装することになります。たぶん。
muffin.js のヘルパー関数(全部じゃない)は、返り値に q.Promise というオブジェクトを返します。ユーザーは、それをq のコールバックのスタックに登録して、コールバックを実装するんです。

muffin.js のユースケース

で、僕のユースケースです。僕がやりたかったのは、

  • Node.js プロジェクト。express ベースです。
  • serverside のcoffee ファイル、 ./resources/server/**/*.coffee を、 ./lib/ の下に 配備したいです。
  • clientside は、 ./resources/client/**/*.coffee を、いろいろしたいです。
    • まず、自分で書いたCoffeescript を、 join して、 compile して、my.js をつくります
    • そのあと、jQuery とか underscore とかのライブラリと、ファイルを一緒にします(concat)。 > ./public/javascripts/client.js
  • ローカルで開発するとき。
  • 本番用にコンパイル(clientside script をminify)して配備するとき。
    • cake -m build で、my.js をminify したものを他とconcat して > ./public/javascripts/client.min.js (大差ないが。)
    • (git への commit とかも、ここでやれるんでしょうね)

それでこんなCakefile を書きました。
https://github.com/piglovesyou/lastfirst/blob/master/Cakefile
追記: stylus はおかしいので外しました。express ベースで使うように変更しています。
Q のコールバックの引数が間違っていたので、修正しました
最初のコードでは、client side のファイル編集をwatch できてなかったので修正しました

# Include required libraries.

muffin = require 'muffin'
Q = require 'q'
_ = require 'underscore'
temp = require 'temp'
tempdir = temp.mkdirSync()



# Client scripts setting.

addPath = (path, files) ->
  path += '/'  if /[^\/]$/.test(path)
  files[_i] = path + file  for file in files

LIBS = addPath 'resources/client/libs', [
  'socket.io.js'
  'underscore-min.js'
  'jquery-1.7.min.js'
]
FILES = addPath 'resources', [
  'share/ext_validate.coffee'
  'client/utils/utils.coffee'
  'client/classes/classes.coffee'
  'client/init/init.coffee'
]
OUTPUT = "public/javascripts/client"



# Internal functions.

outputResult = (result) ->
  out = result[0]
  err = result[1]
  if not err and out
    console.log out
  err

concat = (minify) ->
  min = if minify then '.min' else ''
  my = "#{tempdir}/my#{min}.js"
  q = muffin.exec "cat #{LIBS.join ' '} #{my} > #{OUTPUT}#{min}.js"
  Q.when q[1], outputResult
  
minify = (callback) ->
  q = muffin.minifyScript "#{tempdir}/my.js"
  Q.when q, concat.bind(null, false)

joinAndCompile = (options) ->
  q = muffin.exec "coffee -cj #{tempdir}/my.js #{FILES.join ' '}"  
  Q.when q[1], (result) ->
    err = outputResult(result)
    unless err
      if options.minify
        minify(concat.bind(null, true))
      else
        concat(false)

compileStylus = (file) ->
  q = muffin.exec "stylus -c -o ./public/stylesheets/ #{file}"
  Q.when q[1], outputResult




# Options.

option '-w', '--watch', 'continue to watch the files and rebuild them when they change'
option '-m', '--minify', 'minify client side scripts'



# Tasks.

task 'build', 'Build coffeescripts.', (options) ->
  compileClientScripts = true
  muffin.run
    files: './**/*'
    options: options
    map:
      'app.coffee': (matches) ->
        muffin.compileScript matches[0], "./app.js", options

      'resources/(server|share)/(.+?).coffee': (matches) ->
        muffin.compileScript matches[0], "./lib/#{matches[2]}.js", options

      'resources/(client|share)/(.+?).coffee': (matches) ->
        if compileClientScripts
          compileClientScripts = false  # To prevent wasted compiles
          joinAndCompile(options)

      'resources/client/stylus/(.+?).styl': (matches) ->
        compileStylus matches[0]

    after: ->
      compileClientScripts = true

はてなCoffeescript をsyntax highlight できないんでしょうか。。)
※追記 mizchiさんに助言いただき、rubyでハイライトしてみました。

知ってることを説明してみます。

  • 必須。task の中で、 muffin.run します。
    • 引数にObject を1個(options)わたす。最低3つの キーが必要。
      • files: 操作対象のfile 。string / array。wild card がつかえる。たとえば './resources/**/*.coffee'
      • options: コマンドラインから渡したオプションを、ここでそのまま渡す。あるいは、味付けして渡す。
      • map: Object。(key)ファイル名、(value)そのファイルにしたいfunction。
        • ファイル名は、まっさらなファイルpath string か、 new RegExt(この部分のstring)。
        • value の function には引数が1個わたされる。string.replace の第2引数にわたされるのと多分同じ。ヒットした内容を、function 内で使うため。
      • (なくてもOK)after: files に対する作業がおわったら走る function。
  • muffin.compileScript などで、対象ファイルを操作する。
    • 詳しいことは https://github.com/hornairs/muffin に書いてある。
    • !返り値が、 'q.Promise' objects 。これを使ってcallback をかく。
      • q をちゃんと理解できてないです。あとでちゃんとよむ。
  • muffin.exec 。child_process.exec とほぼおなじ。
    • command line を走らせてくれる。
    • !返り値は配列になってて、[ child_process, q.Promise ] になってる。

な感じです!